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/CONTRIBUTING.md b/.github/CONTRIBUTING.md
similarity index 100%
rename from CONTRIBUTING.md
rename to .github/CONTRIBUTING.md
diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
similarity index 100%
rename from ISSUE_TEMPLATE.md
rename to .github/ISSUE_TEMPLATE.md
diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
similarity index 100%
rename from PULL_REQUEST_TEMPLATE.md
rename to .github/PULL_REQUEST_TEMPLATE.md
diff --git a/.htaccess b/.htaccess
index 90b9c16a5a8c0..4298b10d9ca7a 100644
--- a/.htaccess
+++ b/.htaccess
@@ -36,7 +36,7 @@
############################################
## adjust memory limit
- php_value memory_limit 768M
+ php_value memory_limit 756M
php_value max_execution_time 18000
############################################
@@ -59,7 +59,7 @@
############################################
## adjust memory limit
- php_value memory_limit 768M
+ php_value memory_limit 756M
php_value max_execution_time 18000
############################################
@@ -203,76 +203,166 @@
RedirectMatch 403 /\.git
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
-
-
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
+
+
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ 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
diff --git a/.htaccess.sample b/.htaccess.sample
index 3b61bb672ec8a..a521a347232f5 100644
--- a/.htaccess.sample
+++ b/.htaccess.sample
@@ -35,7 +35,7 @@
############################################
## adjust memory limit
- php_value memory_limit 768M
+ php_value memory_limit 756M
php_value max_execution_time 18000
############################################
@@ -111,7 +111,8 @@
############################################
## enable rewrites
- Options +FollowSymLinks
+ # The following line has better security but add some performance overhead - see https://httpd.apache.org/docs/2.4/en/misc/perf-tuning.html
+ Options -FollowSymLinks +SymLinksIfOwnerMatch
RewriteEngine on
############################################
@@ -179,76 +180,166 @@
RedirectMatch 403 /\.git
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
-
-
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
+
+
+
+ order allow,deny
+ deny from all
+
+ = 2.4>
+ Require all denied
+
- order allow,deny
- deny from all
+
+ 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
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/.user.ini b/.user.ini
index 8c0b765e0551c..bfc3a86d88e20 100644
--- a/.user.ini
+++ b/.user.ini
@@ -1,4 +1,4 @@
-memory_limit = 768M
+memory_limit = 756M
max_execution_time = 18000
session.auto_start = off
suhosin.session.cryptua = off
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef841ec0337f2..a5e94e46f89d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,919 @@
+2.2.2
+=============
+* GitHub issues:
+ * [#9968](https://github.com/magento/magento2/issues/9968) -- Canceled invoice can be canceled again (fixed in [#11261](https://github.com/magento/magento2/pull/11261))
+ * [#11310](https://github.com/magento/magento2/issues/11310) -- Method "getChildren" sort ordering (fixed in [#11342](https://github.com/magento/magento2/pull/11342))
+ * [#11332](https://github.com/magento/magento2/issues/11332) -- How to Fix the wrong input format of Customer date of birth (fixed in [#11351](https://github.com/magento/magento2/pull/11351))
+ * [#11207](https://github.com/magento/magento2/issues/11207) -- Shipment API won't append comment to email (fixed in [#11383](https://github.com/magento/magento2/pull/11383))
+ * [#10795](https://github.com/magento/magento2/issues/10795) -- Shipping method radios have duplicate IDs on cart page (fixed in [#11406](https://github.com/magento/magento2/pull/11406))
+ * [#10941](https://github.com/magento/magento2/issues/10941) -- Responsive Design Issue on Mobile with Magento 2.1.9 (fixed in [#11430](https://github.com/magento/magento2/pull/11430))
+ * [#10007](https://github.com/magento/magento2/issues/10007) -- ProductAlert: Product alerts not showing in admin side product edit page (fixed in [#11445](https://github.com/magento/magento2/pull/11445))
+ * [#10231](https://github.com/magento/magento2/issues/10231) -- Custom URL Rewrite Not working (fixed in [#11470](https://github.com/magento/magento2/pull/11470))
+ * [#11176](https://github.com/magento/magento2/issues/11176) -- Configured table prefix is not recognized in CLI admin:user:create (fixed in [#11199](https://github.com/magento/magento2/pull/11199))
+ * [#11275](https://github.com/magento/magento2/issues/11275) -- Call to a member function addCrumb() (fixed in [#11299](https://github.com/magento/magento2/pull/11299))
+ * [#10441](https://github.com/magento/magento2/issues/10441) -- State/Province Not displayed after edit Billing Address on Sales Orders - Backend Admin. (fixed in [#11381](https://github.com/magento/magento2/pull/11381))
+ * [#11140](https://github.com/magento/magento2/issues/11140) -- Going to '/admin' while using storecodes in url and a different adminhtml url will throw exception (fixed in [#11460](https://github.com/magento/magento2/pull/11460))
+ * [#10765](https://github.com/magento/magento2/issues/10765) -- Export data from grid not adding custom rendered data magento2 (fixed in [#11437](https://github.com/magento/magento2/pull/11437))
+ * [#7678](https://github.com/magento/magento2/issues/7678) -- StockItemCriteria::setProductsFilter doesn't work with array of ids (fixed in [#11500](https://github.com/magento/magento2/pull/11500))
+ * [#9783](https://github.com/magento/magento2/issues/9783) -- Multiple parameters in widget.xml not allowed (fixed in [#11495](https://github.com/magento/magento2/pull/11495))
+ * [#10824](https://github.com/magento/magento2/issues/10824) -- Cannot add new columns to item grid in admin sales_order_view layout (fixed in [#11235](https://github.com/magento/magento2/pull/11235))
+ * [#9919](https://github.com/magento/magento2/issues/9919) -- Pattern Validation via UI Component Fails to Interpret String as RegEx Pattern (fixed in [#11565](https://github.com/magento/magento2/pull/11565))
+ * [#5439](https://github.com/magento/magento2/issues/5439) -- Newsletter subscription (fixed in [#11317](https://github.com/magento/magento2/pull/11317))
+ * [#10856](https://github.com/magento/magento2/issues/10856) -- Sync billing with shipping address on Admin Reorder and Admin Customer Create Order page does not work for Existing address selected (fixed in [#11385](https://github.com/magento/magento2/pull/11385))
+ * [#10025](https://github.com/magento/magento2/issues/10025) -- Integration tests don't reset the database (fixed in [#11499](https://github.com/magento/magento2/pull/11499))
+ * [#10301](https://github.com/magento/magento2/issues/10301) -- Customer review report search Bug in 2.1.x, 2.2 (fixed in [#11522](https://github.com/magento/magento2/pull/11522))
+ * [#11540](https://github.com/magento/magento2/issues/11540) -- Magento sets iso invalid language code in html header (fixed in [#11561](https://github.com/magento/magento2/pull/11561))
+ * [#11586](https://github.com/magento/magento2/issues/11586) -- Cron install / remove via command messes up stderr 2>&1 entries (fixed in [#11591](https://github.com/magento/magento2/pull/11591))
+ * [#6350](https://github.com/magento/magento2/issues/6350) -- Frontend: Datepicker/calendar control does not use the store locale (fixed in [#11057](https://github.com/magento/magento2/pull/11057))
+ * [#11328](https://github.com/magento/magento2/issues/11328) -- app:config:dump adds extra space every time in multiline array value (fixed in [#11439](https://github.com/magento/magento2/pull/11439))
+ * [#7591](https://github.com/magento/magento2/issues/7591) -- PayPal module, "didgit" misspelling (fixed in [#11673](https://github.com/magento/magento2/pull/11673))
+ * [#7767](https://github.com/magento/magento2/issues/7767) -- in system.xml translate phrase not work (fixed in [#11675](https://github.com/magento/magento2/pull/11675))
+ * [#7915](https://github.com/magento/magento2/issues/7915) -- customer objects are equal to eachother after observing event customer_save_after_data_object (fixed in [#11676](https://github.com/magento/magento2/pull/11676))
+ * [#10275](https://github.com/magento/magento2/issues/10275) -- Admin global search - submit by enter doesn't work (fixed in [#11250](https://github.com/magento/magento2/pull/11250))
+ * [#11022](https://github.com/magento/magento2/issues/11022) -- GET v1/products/attribute-sets/sets/list inconsistent return result (fixed in [#11421](https://github.com/magento/magento2/pull/11421))
+ * [#5956](https://github.com/magento/magento2/issues/5956) -- Untranslatable string "Please enter the same value again." (fixed in [#11440](https://github.com/magento/magento2/pull/11440))
+ * [#9944](https://github.com/magento/magento2/issues/9944) -- Name attribute shows empty when creating custom fields on product creation form (fixed in [#11637](https://github.com/magento/magento2/pull/11637))
+ * [#10168](https://github.com/magento/magento2/issues/10168) -- Coupon codes not showing in invoice (fixed in [#11635](https://github.com/magento/magento2/pull/11635))
+ * [#9763](https://github.com/magento/magento2/issues/9763) -- When go checkout,Cart Price Rules 25%test coupon code can go wrong (fixed in [#11710](https://github.com/magento/magento2/pull/11710))
+ * [#11157](https://github.com/magento/magento2/issues/11157) -- nginx.sample.conf missing heath_check.php? (fixed in [#11690](https://github.com/magento/magento2/pull/11690))
+ * [#11322](https://github.com/magento/magento2/issues/11322) -- User.ini files specify 768M - Docs recommend at least 1G (fixed in [#11734](https://github.com/magento/magento2/pull/11734))
+ * [#7927](https://github.com/magento/magento2/issues/7927) -- Dashboard graph has broken y-axis range (fixed in [#11751](https://github.com/magento/magento2/pull/11751))
+ * [#7099](https://github.com/magento/magento2/issues/7099) -- Admin: field labels wrapping poorly (fixed in [#11745](https://github.com/magento/magento2/pull/11745))
+ * [#9869](https://github.com/magento/magento2/issues/9869) -- datetime type product attribute showing current date (fixed in [#11749](https://github.com/magento/magento2/pull/11749))
+ * [#11365](https://github.com/magento/magento2/issues/11365) -- "Ignore this notification" isn't working (fixed in [#11410](https://github.com/magento/magento2/pull/11410))
+ * [#6891](https://github.com/magento/magento2/issues/6891) -- Add-to-cart checkbox still visible when $canItemsAddToCart = false (fixed in [#11610](https://github.com/magento/magento2/pull/11610))
+ * [#11729](https://github.com/magento/magento2/issues/11729) -- Exported Excel with negative number can't be opened by MS Office (fixed in [#11757](https://github.com/magento/magento2/pull/11757))
+ * [#6924](https://github.com/magento/magento2/issues/6924) -- Magento 2.1.0 - "General system exception happened" on Import .csv (fixed in [#11363](https://github.com/magento/magento2/pull/11363))
+ * [#7640](https://github.com/magento/magento2/issues/7640) -- X-Magento-Tags header containing whitespaces causes exception (fixed in [#11767](https://github.com/magento/magento2/pull/11767))
+ * [#4711](https://github.com/magento/magento2/issues/4711) -- Improve error reporting for products images import (fixed in [#11779](https://github.com/magento/magento2/pull/11779))
+ * [#4696](https://github.com/magento/magento2/issues/4696) -- Admin product search - Pressing enter does not submit (fixed in [#11827](https://github.com/magento/magento2/pull/11827))
+ * [#11581](https://github.com/magento/magento2/issues/11581) -- Reference to wrong / non-existing class (fixed in [#11830](https://github.com/magento/magento2/pull/11830))
+ * [#10908](https://github.com/magento/magento2/issues/10908) -- [2.2.0-rc3.0] Language switcher is broken when using multiple times (fixed in [#11337](https://github.com/magento/magento2/pull/11337))
+ * [#11211](https://github.com/magento/magento2/issues/11211) -- Store View switcher not working on front-end and it throws an error (fixed in [#11337](https://github.com/magento/magento2/pull/11337))
+ * [#2991](https://github.com/magento/magento2/issues/2991) -- Products added to cart with REST API give total prices equal to zero (fixed in [#11458](https://github.com/magento/magento2/pull/11458))
+ * [#10032](https://github.com/magento/magento2/issues/10032) -- Download back-up .tgz always takes the latest that's created (fixed in [#11595](https://github.com/magento/magento2/pull/11595))
+ * [#11534](https://github.com/magento/magento2/issues/11534) -- Values of Visual Swatch Attribute drop down is not work correct (fixed in [#11747](https://github.com/magento/magento2/pull/11747))
+ * [#10291](https://github.com/magento/magento2/issues/10291) -- Magento 2 Loading custom option dropdown issue (fixed in [#11824](https://github.com/magento/magento2/pull/11824))
+ * [#11095](https://github.com/magento/magento2/issues/11095) -- Magento_Tax "postcode is a required field" when upgrading from 2.1.9 to 2.2 (fixed in [#11651](https://github.com/magento/magento2/pull/11651))
+ * [#8236](https://github.com/magento/magento2/issues/8236) -- CMS blocks are not validated against having same store and identifier (fixed in [#11802](https://github.com/magento/magento2/pull/11802))
+ * [#4808](https://github.com/magento/magento2/issues/4808) -- The price of product custom option can't be set to 0. (fixed in [#11843](https://github.com/magento/magento2/pull/11843))
+ * [#9566](https://github.com/magento/magento2/issues/9566) -- Status label is wrong in admin (fixed in [#11397](https://github.com/magento/magento2/pull/11397))
+ * [#5015](https://github.com/magento/magento2/issues/5015) -- Report error csv doesn't work when trying to import a csv file with semicolon delimiter (fixed in [#11732](https://github.com/magento/magento2/pull/11732))
+ * [#10682](https://github.com/magento/magento2/issues/10682) -- Meta description and keywords transform to html entities for non latin/cyrilic characters in category and product pages (fixed in [#11829](https://github.com/magento/magento2/pull/11829))
+ * [#10185](https://github.com/magento/magento2/issues/10185) -- New Orders are not in Order grid after data migration from M 1.7.0.2 to M 2.1.7 (fixed in [#11911](https://github.com/magento/magento2/pull/11911))
+ * [#8970](https://github.com/magento/magento2/issues/8970) -- Cannot assign products to categories not under tree root (fixed in [#11817](https://github.com/magento/magento2/pull/11817))
+ * [#9028](https://github.com/magento/magento2/issues/9028) -- You cannot set a 303 redirect response using a result factory (fixed in [#11405](https://github.com/magento/magento2/pull/11405))
+ * [#11697](https://github.com/magento/magento2/issues/11697) -- Theme: Added html node to page xml root, cause validation error (fixed in [#11858](https://github.com/magento/magento2/pull/11858))
+ * [#8954](https://github.com/magento/magento2/issues/8954) -- Error While Trying To Load Quote Item Collection Using Magento\Quote\Model\ResourceModel\QuoteItem\Collection::getItems() (fixed in [#11869](https://github.com/magento/magento2/pull/11869))
+ * [#8799](https://github.com/magento/magento2/issues/8799) -- Image brackground (fixed in [#11889](https://github.com/magento/magento2/pull/11889))
+ * [#11868](https://github.com/magento/magento2/issues/11868) -- "Add Products" button has been duplicated after the customer group was changed (fixed in [#11949](https://github.com/magento/magento2/pull/11949))
+ * [#11898](https://github.com/magento/magento2/issues/11898) -- Zip code Netherlands should allow zipcode without space (fixed in [#11959](https://github.com/magento/magento2/pull/11959))
+ * [#11996](https://github.com/magento/magento2/issues/11996) -- Magento 2 Store Code validation regex: doesn't support uppercase letters in store code (fixed in [#12011](https://github.com/magento/magento2/pull/12011))
+ * [#7995](https://github.com/magento/magento2/issues/7995) -- If you leave as default, shipping lines disappear (fixed in [#12013](https://github.com/magento/magento2/pull/12013))
+ * [#8846](https://github.com/magento/magento2/issues/8846) -- Attribute option value uniqueness is not checked if created via REST Api (fixed in [#11785](https://github.com/magento/magento2/pull/11785))
+ * [#11700](https://github.com/magento/magento2/issues/11700) -- "Something Went Wrong" error for limited access admin user (fixed in [#11993](https://github.com/magento/magento2/pull/11993))
+ * [#12017](https://github.com/magento/magento2/issues/12017) -- Cross-sell product placeholder image size issue (fixed in [#12018](https://github.com/magento/magento2/pull/12018))
+ * [#10583](https://github.com/magento/magento2/issues/10583) -- Checkout place order exception when using a new address (fixed in [#11556](https://github.com/magento/magento2/pull/11556))
+ * [#4004](https://github.com/magento/magento2/issues/4004) -- Newsletter Subscriber create-date not set, and change_status_at broken (fixed in [#11879](https://github.com/magento/magento2/pull/11879))
+ * [#7225](https://github.com/magento/magento2/issues/7225) -- [BUG] [Magento 2.1.2] Programmatically creating an empty dropdown attribute, "apply_to" is set to NULL (from "simple") after adding options through store admin (fixed in [#11588](https://github.com/magento/magento2/pull/11588))
+ * [#11197](https://github.com/magento/magento2/issues/11197) -- Blank page at the checkout 'shipping' step (fixed in [#11958](https://github.com/magento/magento2/pull/11958))
+ * [#11880](https://github.com/magento/magento2/issues/11880) -- Magento 2.1.9 Configurable::getUsedProducts returns a different array after product collections is cached (fixed in [#12107](https://github.com/magento/magento2/pull/12107))
+ * [#10811](https://github.com/magento/magento2/issues/10811) -- Replace FollowSymLinks with SymLinksIfOwnerMatch (fixed in [#11461](https://github.com/magento/magento2/pull/11461))
+ * [#10920](https://github.com/magento/magento2/issues/10920) -- Sku => Entity_id relations are fetched inefficiently when inserting attributes values during product import (fixed in [#11719](https://github.com/magento/magento2/pull/11719))
+ * [#6802](https://github.com/magento/magento2/issues/6802) -- Magento\Search\Helper\getSuggestUrl() not used in search template (fixed in [#11722](https://github.com/magento/magento2/pull/11722))
+ * [#9151](https://github.com/magento/magento2/issues/9151) -- Sitemap.xml: lastmod timestamp can contain invalid dates (fixed in [#11902](https://github.com/magento/magento2/pull/11902))
+ * [#10195](https://github.com/magento/magento2/issues/10195) -- Order relation child is not set during edit operation. (fixed in [#11988](https://github.com/magento/magento2/pull/11988))
+ * [#11793](https://github.com/magento/magento2/issues/11793) -- Magento2.1.5 admin shipping report shows wrong currency code (fixed in [#11962](https://github.com/magento/magento2/pull/11962))
+ * [#6661](https://github.com/magento/magento2/issues/6661) -- XHTML templates Don't Use Schema URNs (fixed in [#12031](https://github.com/magento/magento2/pull/12031))
+ * [#12079](https://github.com/magento/magento2/issues/12079) -- Products in cart report error when we have grouped or bundle product (fixed in [#12082](https://github.com/magento/magento2/pull/12082))
+ * [#9768](https://github.com/magento/magento2/issues/9768) -- Admin dashboard Most Viewed Products Tab only gives default attribute set's products (fixed in [#12139](https://github.com/magento/magento2/pull/12139))
+ * [#6238](https://github.com/magento/magento2/issues/6238) -- Meta description allows too many characters (fixed in [#11914](https://github.com/magento/magento2/pull/11914))
+ * [#11230](https://github.com/magento/magento2/issues/11230) -- Unit test fails after fresh installation (fixed in [#12144](https://github.com/magento/magento2/pull/12144))
+ * [#10810](https://github.com/magento/magento2/issues/10810) -- Add support of apache2.4 commands in htaccess (fixed in [#11459](https://github.com/magento/magento2/pull/11459))
+ * [#10834](https://github.com/magento/magento2/issues/10834) -- signing in after selecting checkout button, will not end up to checkout page! (fixed in [#11876](https://github.com/magento/magento2/pull/11876))
+ * [#10477](https://github.com/magento/magento2/issues/10477) -- Cart price rule has failed if use dropdown attribute (fixed in [#11274](https://github.com/magento/magento2/pull/11274))
+ * [#11832](https://github.com/magento/magento2/issues/11832) -- Create order (on Customer edit page) - not working from admin environment (fixed in [#11952](https://github.com/magento/magento2/pull/11952))
+ * [#10014](https://github.com/magento/magento2/issues/10014) -- Newsletter subscriptions status not isolated between multi stores (fixed in [#12035](https://github.com/magento/magento2/pull/12035))
+ * [#11532](https://github.com/magento/magento2/issues/11532) -- Duplicate Simple Product Throws Error: Undefined offset: 0 in SaveHandler.php on line 122 (fixed in [#12001](https://github.com/magento/magento2/pull/12001))
+ * [#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)
+ * [#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)
+ * [#11359](https://github.com/magento/magento2/pull/11359) -- [Backport 2.2-develop] Unable to manage (install/uninstall) cron via bin/magento cron:install / cron:remove with multiple installations against same crontab (by @adrian-martinez-interactiv4)
+ * [#11383](https://github.com/magento/magento2/pull/11383) -- Append shipment comment to shipment if appendComment is true (by @JeroenVanLeusden)
+ * [#11406](https://github.com/magento/magento2/pull/11406) -- Added carrier code to ID to distinguish shipping methods (by @peterjaap)
+ * [#11430](https://github.com/magento/magento2/pull/11430) -- Fix toolbar-amount placing in mobile device (by @slackerzz)
+ * [#11445](https://github.com/magento/magento2/pull/11445) -- Show product alerts in admin product detail [backport 2.2] (by @raumatbel)
+ * [#11470](https://github.com/magento/magento2/pull/11470) -- FR#10231_22 Custom URL Rewrite Not working [backport 2.2] (by @mrodespin)
+ * [#11493](https://github.com/magento/magento2/pull/11493) -- Add "optional" translation in checkout password field (by @JeroenVanLeusden)
+ * [#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)
+ * [#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)
+ * [#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)
+ * [#11569](https://github.com/magento/magento2/pull/11569) -- Fixed double space typo (by @dverkade)
+ * [#11565](https://github.com/magento/magento2/pull/11565) -- Fix "pattern" UI Component validation (by @bap14)
+ * [#11317](https://github.com/magento/magento2/pull/11317) -- [Backport 2.2-develop] Send email to subscribers only when are new (by @osrecio)
+ * [#11385](https://github.com/magento/magento2/pull/11385) -- Fix #10856: Sync billing with shipping address on Admin Order Page (by @joni-jones)
+ * [#11499](https://github.com/magento/magento2/pull/11499) -- Ensure database is cleared/Magento reinstalled when TESTS_CLEANUP is enabled (by @joshuaswarren)
+ * [#11510](https://github.com/magento/magento2/pull/11510) -- Add interaction to admin:user:create command (by @cmuench)
+ * [#11522](https://github.com/magento/magento2/pull/11522) -- [Backport 2.2-develop] Fix Filter Customer Report Review (by @osrecio)
+ * [#11553](https://github.com/magento/magento2/pull/11553) -- [2.2 Backport] ProductRepository sku cache is corrupted when cacheLimit is reached (by @heldchen)
+ * [#11561](https://github.com/magento/magento2/pull/11561) -- Magento sets ISO invalid language code (by @crissanclick)
+ * [#11591](https://github.com/magento/magento2/pull/11591) -- [Backport 2.2-develop] #11586 Fix duplicated crontab 2>&1 expression (by @adrian-martinez-interactiv4)
+ * [#11439](https://github.com/magento/magento2/pull/11439) -- [Backport 2.2-develop] #11328 : app:config:dump adds extra space every time in multiline array value (by @adrian-martinez-interactiv4)
+ * [#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)
+ * [#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)
+ * [#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)
+ * [#11720](https://github.com/magento/magento2/pull/11720) -- Fix Notice: freePackageValue is undefined (by @amenk)
+ * [#11734](https://github.com/magento/magento2/pull/11734) -- [TASK] Updated user.ini according to Magento DevDocs (by @lewisvoncken)
+ * [#11751](https://github.com/magento/magento2/pull/11751) -- [Backport 2.2-develop] Dashboard Fix Y Axis for range (by @osrecio)
+ * [#11749](https://github.com/magento/magento2/pull/11749) -- [Backport 2.2-develop] Fix datetime type product that show current date when is empty in grids (by @enriquei4)
+ * [#11745](https://github.com/magento/magento2/pull/11745) -- [Backport 2.2-develop] Fix label to avoid wrapping poorly,now break by word (by @enriquei4)
+ * [#11765](https://github.com/magento/magento2/pull/11765) -- Allows modules with underscores in name to add blocks to layout via XML (by @bentideswell)
+ * [#11410](https://github.com/magento/magento2/pull/11410) -- "Ignore this notification" isn't working (by @crissanclick)
+ * [#11607](https://github.com/magento/magento2/pull/11607) -- [Backport 2.2-develop] Fix AcountManagementTest unit test fail randomly (by @adrian-martinez-interactiv4)
+ * [#11610](https://github.com/magento/magento2/pull/11610) -- FR#6891_22 Add-to-cart checkbox still visible when = false (by @mrodespin)
+ * [#11757](https://github.com/magento/magento2/pull/11757) -- Fix #11729 - negative value in excel export [M2.2] (by @hauso)
+ * [#11363](https://github.com/magento/magento2/pull/11363) -- Issue #6924: Unmask exception message during product import (by @tim-bezhashvyly)
+ * [#11425](https://github.com/magento/magento2/pull/11425) -- Magento setup:install interactive shell (by @denisristic)
+ * [#11767](https://github.com/magento/magento2/pull/11767) -- 7640: X-Magento-Tags header containing whitespaces causes exception (by @nmalevanec)
+ * [#11779](https://github.com/magento/magento2/pull/11779) -- MAGETWO-4711: Improve error reporting for products images import. (by @p-bystritsky)
+ * [#11830](https://github.com/magento/magento2/pull/11830) -- Fix #11581: Reference to wrong / non-existing class (by @dverkade)
+ * [#11827](https://github.com/magento/magento2/pull/11827) -- Admin product search - Pressing enter does not submit #4696 (by @bohemiorulo)
+ * [#11337](https://github.com/magento/magento2/pull/11337) -- #11211 Fix Store View switcher (by @thiagolima-bm)
+ * [#11458](https://github.com/magento/magento2/pull/11458) -- Products added to cart with REST API give total prices equal to zero (by @peterjaap)
+ * [#11595](https://github.com/magento/magento2/pull/11595) -- Fix issue #10032 - Download back-up .tgz always takes the latest that's created (2.2-develop) (by @PieterCappelle)
+ * [#11747](https://github.com/magento/magento2/pull/11747) -- [Backport 2.2-develop] FIX show visual swatches in admin - product attribute (by @enriquei4)
+ * [#11824](https://github.com/magento/magento2/pull/11824) -- Magetwo 70954: Remove the component.clear from the custom options type. This causes the 'elem' array to become out of sync with the recordData (by @briscoda)
+ * [#11651](https://github.com/magento/magento2/pull/11651) -- [BUGFIX] Solved error while upgrading from 2.1 to 2.2 (by @lewisvoncken)
+ * [#11802](https://github.com/magento/magento2/pull/11802) -- #8236 FIX CMS blocks (by @thiagolima-bm)
+ * [#11843](https://github.com/magento/magento2/pull/11843) -- Save the price 0 as price in custom options [backport 2.2] (by @raumatbel)
+ * [#11854](https://github.com/magento/magento2/pull/11854) -- FilterBuilder Doc Block Update (by @ByteCreation)
+ * [#11397](https://github.com/magento/magento2/pull/11397) -- Fix for #9566: Show the correct label in the admin (by @michielgerritsen)
+ * [#11732](https://github.com/magento/magento2/pull/11732) -- MAGETWO-5015: Report error csv doesn't work when trying to import a csv file with semicolon delimiter. (by @p-bystritsky)
+ * [#11829](https://github.com/magento/magento2/pull/11829) -- Fix #10682: Meta description and keywords transform to html entities (by @dverkade)
+ * [#11933](https://github.com/magento/magento2/pull/11933) -- Changed constructor typo in Javascript class (by @dverkade)
+ * [#11911](https://github.com/magento/magento2/pull/11911) -- Order grid - Sort by Purchase Date Desc by default (by @ihor-sviziev)
+ * [#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)
+ * [#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)
+ * [#11959](https://github.com/magento/magento2/pull/11959) -- #11898 - Change NL PostCode Pattern (by @osrecio)
+ * [#11620](https://github.com/magento/magento2/pull/11620) -- Check attribute unique between same fields in magento commerce (by @raumatbel)
+ * [#11770](https://github.com/magento/magento2/pull/11770) -- Product attribute creation page handles Storefront tab visibility wrong (by @euronetzrt)
+ * [#11863](https://github.com/magento/magento2/pull/11863) -- Update wrong layout update xml handle installed in CMS Home Page by default (by @adrian-martinez-interactiv4)
+ * [#12011](https://github.com/magento/magento2/pull/12011) -- [backport 2.2] Magento 2 Store Code validation regex: doesn't support uppercase letters in store code (by @manuelson)
+ * [#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)
+ * [#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)
+ * [#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)
+ * [#11902](https://github.com/magento/magento2/pull/11902) -- #9151: [Github] Sitemap.xml: lastmod timestamp can contain invalid dates (by @serhii-balko)
+ * [#11947](https://github.com/magento/magento2/pull/11947) -- Fix json encoded attribute backend type when attribute value is null (by @tkotosz)
+ * [#11962](https://github.com/magento/magento2/pull/11962) -- 11793: Magento2.1.5 admin shipping report shows wrong currency code (by @RomaKis)
+ * [#11988](https://github.com/magento/magento2/pull/11988) -- 10195: Order relation child is not set during edit operation(backport from 2.3 to 2.2) (by @RomaKis)
+ * [#12031](https://github.com/magento/magento2/pull/12031) -- Improve urn in xhtml (by @enriquei4)
+ * [#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)
+ * [#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)
+ * [#11968](https://github.com/magento/magento2/pull/11968) -- Fix bug: Customer import deletes exiting customer entity Fields (by @jalogut)
+ * [#12061](https://github.com/magento/magento2/pull/12061) -- Cleanup for object manager references and depricated method (by @atishgoswami)
+ * [#12136](https://github.com/magento/magento2/pull/12136) -- update button.phtml overcomplicated translation phrase. 2.2 (by @ChuckyK)
+ * [#11876](https://github.com/magento/magento2/pull/11876) -- After logging in customer is now not redirecting to Customer Dashboard by default (by @p-bystritsky)
+ * [#11274](https://github.com/magento/magento2/pull/11274) -- Fix #10477 Check cart rule subselect conditions against quote item children too (by @marinagociu)
+ * [#11952](https://github.com/magento/magento2/pull/11952) -- 11832: Create order (on Customer edit page) - not working from admin environment (by @RomaKis)
+ * [#12035](https://github.com/magento/magento2/pull/12035) -- Fix newsletter subscriptions between stores (by @sbaixauli)
+ * [#12001](https://github.com/magento/magento2/pull/12001) -- 11532: Duplicate Simple Product Throws Error: Undefined offset: 0 in SaveHandler.php on line 122 (by @RomaKis)
+ * [#12077](https://github.com/magento/magento2/pull/12077) -- 10628: Color attribute swatches are not visible if sorting is enabled (by @RomaKis)
+ * [#12130](https://github.com/magento/magento2/pull/12130) -- [Backport 2.2] MAGETWO-71697: Fix possible bug when saving address with empty street line #10582 (by @vovayatsyuk)
+ * [#12141](https://github.com/magento/magento2/pull/12141) -- Fix js error when disable/enable wysiwyg editor (by @vovayatsyuk)
+ * [#12173](https://github.com/magento/magento2/pull/12173) -- 8022: Uncaught Error: Call to a member function addItem() on array in app/code/Magento/Sales/Model/Order/Shipment.php(backport to 2.2) (by @RomaKis)
+
+2.2.1
+=============
+* GitHub issues:
+ * [#4248](https://github.com/magento/magento2/issues/4248) -- Validations not working on customer registration on DOB field. (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#6350](https://github.com/magento/magento2/issues/6350) -- Frontend: Datepicker/calendar control does not use the store locale (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#6858](https://github.com/magento/magento2/issues/6858) -- DatePicker date format does not reflect user's locale (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#6831](https://github.com/magento/magento2/issues/6831) -- Magento 2.1.1 Invalid input date format 'Invalid date' (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#9743](https://github.com/magento/magento2/issues/9743) -- Invalid date when customer validate with French locale (fixed in [#11067](https://github.com/magento/magento2/pull/11067))
+ * [#6712](https://github.com/magento/magento2/issues/6712) -- Footer Links Widget CSS Issue (fixed in [#11063](https://github.com/magento/magento2/pull/11063))
+ * [#9008](https://github.com/magento/magento2/issues/9008) -- Error Message Is Confusing When Code Base Is Behind Database Module Version (fixed in [#11064](https://github.com/magento/magento2/pull/11064))
+ * [#9981](https://github.com/magento/magento2/issues/9981) -- M2 suggests running setup:upgrade if version number of module is higher than expected (fixed in [#11064](https://github.com/magento/magento2/pull/11064))
+ * [#10824](https://github.com/magento/magento2/issues/10824) -- Cannot add new columns to item grid in admin sales_order_view layout (fixed in [#11076](https://github.com/magento/magento2/pull/11076))
+ * [#10417](https://github.com/magento/magento2/issues/10417) -- Wysywig editor shows broken image icons (fixed in [#11048](https://github.com/magento/magento2/pull/11048))
+ * [#10697](https://github.com/magento/magento2/issues/10697) -- Product Import: Additional data: Invalid URL key (fixed in [#11049](https://github.com/magento/magento2/pull/11049))
+ * [#10474](https://github.com/magento/magento2/issues/10474) -- Error message in product review form not being translated (fixed in [#11069](https://github.com/magento/magento2/pull/11069))
+ * [#9877](https://github.com/magento/magento2/issues/9877) -- getCacheTags for price issue (fixed in [#11154](https://github.com/magento/magento2/pull/11154))
+ * [#10738](https://github.com/magento/magento2/issues/10738) -- Empty attribute label is displayed on product page when other language used. (fixed in [#11168](https://github.com/magento/magento2/pull/11168))
+ * [#9900](https://github.com/magento/magento2/issues/9900) -- Cms module collections missing event prefix (fixed in [#11223](https://github.com/magento/magento2/pull/11223))
+ * [#11044](https://github.com/magento/magento2/issues/11044) -- magento setup:upgrade prompts to run compilation, even in developer mode (fixed in [#11050](https://github.com/magento/magento2/pull/11050))
+ * [#10775](https://github.com/magento/magento2/issues/10775) -- 404 Forbidden sounds not right (fixed in [#11134](https://github.com/magento/magento2/pull/11134))
+ * [#11231](https://github.com/magento/magento2/issues/11231) -- Can't close mobile search bar once typed (fixed in [#11246](https://github.com/magento/magento2/pull/11246))
+ * [#10317](https://github.com/magento/magento2/issues/10317) -- Region is being overridden when changing from a required-state country to one that is not required (fixed in [#11254](https://github.com/magento/magento2/pull/11254))
+ * [#11089](https://github.com/magento/magento2/issues/11089) -- setup:config:set --key append instead of replace (fixed in [#11155](https://github.com/magento/magento2/pull/11155))
+ * [#7582](https://github.com/magento/magento2/issues/7582) -- Payment methods in payments title in wrong language (fixed in [#11165](https://github.com/magento/magento2/pull/11165))
+ * [#5105](https://github.com/magento/magento2/issues/5105) -- Error While send Invoice with Grouped Products (fixed in [#11297](https://github.com/magento/magento2/pull/11297))
+ * [#11163](https://github.com/magento/magento2/issues/11163) -- Magento 2.2.0 Pages showing error: Data key is missing: code-entity (fixed in [#11205](https://github.com/magento/magento2/pull/11205))
+ * [#11329](https://github.com/magento/magento2/issues/11329) -- Unable to proceed massaction "Update attributes" with required multiple select attribute (fixed in [#11349](https://github.com/magento/magento2/pull/11349))
+ * [#8958](https://github.com/magento/magento2/issues/8958) -- Hint mistake in english language (fixed in [#11390](https://github.com/magento/magento2/pull/11390))
+* GitHub pull requests:
+ * [#11067](https://github.com/magento/magento2/pull/11067) -- Fix dob date validation on custom locale (by @joachimVT)
+ * [#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)
+ * [#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)
+ * [#11069](https://github.com/magento/magento2/pull/11069) -- Error message in product review form not being translated (by @Echron)
+ * [#11127](https://github.com/magento/magento2/pull/11127) -- Add missing return statements in setters (by @niccifor)
+ * [#11138](https://github.com/magento/magento2/pull/11138) -- Added template as argument to the store address renderer to allow custom formatting (by @jokeputs)
+ * [#11147](https://github.com/magento/magento2/pull/11147) -- Fix the correct removal of the images and the removal of all images in the catalog (by @raumatbel)
+ * [#11154](https://github.com/magento/magento2/pull/11154) -- Issue #9877: Backport of: getCacheTags for price issue #10930 (by @denysbabenko)
+ * [#11160](https://github.com/magento/magento2/pull/11160) -- Ported Down changes from PR#10919 (by @strell)
+ * [#11200](https://github.com/magento/magento2/pull/11200) -- Delete CallExit function for After plugin logic execution 2.2-develop [BackPort] (by @osrecio)
+ * [#11168](https://github.com/magento/magento2/pull/11168) -- Fixed issue #10738: Don't display attribute label if defined as "none" in layout (by @maksek)
+ * [#11223](https://github.com/magento/magento2/pull/11223) -- Cms page/block eventprefix fix issue 9900 (by @convenient)
+ * [#11229](https://github.com/magento/magento2/pull/11229) -- Disable secret key validation on admin noroute to fix redirect loop (by @convenient)
+ * [#11050](https://github.com/magento/magento2/pull/11050) -- Only prompt for deploy command in production mode (by @schmengler)
+ * [#11134](https://github.com/magento/magento2/pull/11134) -- Fix 404 status header (by @Zifius)
+ * [#11084](https://github.com/magento/magento2/pull/11084) -- Fix in stripped min length validation when value has special characters (by @rubenRP)
+ * [#11246](https://github.com/magento/magento2/pull/11246) -- Can't close mobile search bar (by @crissanclick)
+ * [#11254](https://github.com/magento/magento2/pull/11254) -- Fix for #10317 Disable region list when switching to a region optional country. (by @romainruaud)
+ * [#11155](https://github.com/magento/magento2/pull/11155) -- Refactor ConfigGenerator to replace/set crypt key instead of append (by @renttek)
+ * [#11291](https://github.com/magento/magento2/pull/11291) -- Fix #9243 - Upgrade ZF components. Zend_Service (by @dverkade)
+ * [#11165](https://github.com/magento/magento2/pull/11165) -- #7582: use setStoreId after custom load method to give storeId precedence (by @bka)
+ * [#11297](https://github.com/magento/magento2/pull/11297) -- Fix for issue #5105 - Error While send Invoice with Grouped Products (by @michielgerritsen)
+ * [#11327](https://github.com/magento/magento2/pull/11327) -- Fix #10812: htaccess Options override (by @dverkade)
+ * [#11081](https://github.com/magento/magento2/pull/11081) -- Text updated. (by @RakeshJesadiya)
+ * [#11183](https://github.com/magento/magento2/pull/11183) -- FIX for #11166 Index Handling Fatal Error (by @larsroettig)
+ * [#11205](https://github.com/magento/magento2/pull/11205) -- Fixes #11163 missing data key code-entity in CMS page edit form (by @Tomasz-Silpion)
+ * [#11219](https://github.com/magento/magento2/pull/11219) -- Fix typo in sessionStorage polyfill (by @mszydlo)
+ * [#11249](https://github.com/magento/magento2/pull/11249) -- Add a payload extender to the default shipping-save-processor (Backport to 2.2) (by @navarr)
+ * [#11345](https://github.com/magento/magento2/pull/11345) -- Use US English spelling for "Optimization". (by @davidangel)
+ * [#11349](https://github.com/magento/magento2/pull/11349) -- Bug fix update attributes (by @manuelson)
+ * [#11390](https://github.com/magento/magento2/pull/11390) -- Fix typo in design rule hint message (by @jahvi)
+
+2.2.0
+=============
+* GitHub issues:
+ * [#8287](https://github.com/magento/magento2/issues/8287) -- InputException while updating cart with Minimum order amount enabled (fixed in [#8474](https://github.com/magento/magento2/pull/8474))
+ * [#8315](https://github.com/magento/magento2/issues/8315) -- Error with StdoTest (fixed in [#8487](https://github.com/magento/magento2/pull/8487))
+ * [#5808](https://github.com/magento/magento2/issues/5808) -- [2.1.0] Problem on mobile when catalog gallery allowfullscreen is false (fixed in [#8434](https://github.com/magento/magento2/pull/8434))
+ * [#8392](https://github.com/magento/magento2/issues/8392) -- Initial loading of the related-products sorting fails in Edge-Mode (fixed in [#8467](https://github.com/magento/magento2/pull/8467))
+ * [#8076](https://github.com/magento/magento2/issues/8076) -- Currency Setup in admin throws in_array error when a single value is selected (fixed in [#8077](https://github.com/magento/magento2/pull/8077))
+ * [#8277](https://github.com/magento/magento2/issues/8277) -- CatalogImportExport uploader can't handle HTTPS images (fixed in [#8278](https://github.com/magento/magento2/pull/8278))
+ * [#7353](https://github.com/magento/magento2/issues/7353) -- Crosssells are never shown when using product/list/items.phtml template (fixed in [#8602](https://github.com/magento/magento2/pull/8602))
+ * [#8632](https://github.com/magento/magento2/issues/8632) -- Location of wishlist.js file is incorrect (fixed in [#8633](https://github.com/magento/magento2/pull/8633))
+ * [#6634](https://github.com/magento/magento2/issues/6634) -- Yes/No attribute value is not shown on a product details page (fixed in [#8623](https://github.com/magento/magento2/pull/8623))
+ * [#8566](https://github.com/magento/magento2/issues/8566) -- Setting 'show_out_of_stock' to 'No' has no effect (fixed in [#8736](https://github.com/magento/magento2/pull/8736))
+ * [#6706](https://github.com/magento/magento2/issues/6706) -- Notice undefined index when changes swatch attribute (fixed in [#6707](https://github.com/magento/magento2/pull/6707))
+ * [#6855](https://github.com/magento/magento2/issues/6855) -- Create order via backend doesn't work after using NL translation for order-header (fixed in [#6856](https://github.com/magento/magento2/pull/6856))
+ * [#8515](https://github.com/magento/magento2/issues/8515) -- Downloadable product is available for download even if order state is set canceled. (fixed in [#8917](https://github.com/magento/magento2/pull/8917))
+ * [#8871](https://github.com/magento/magento2/issues/8871) -- Typo in Pull Request Template (fixed in [#8908](https://github.com/magento/magento2/pull/8908))
+ * [#3791](https://github.com/magento/magento2/issues/3791) -- Error review when customer is not logged (fixed in [#9001](https://github.com/magento/magento2/pull/9001))
+ * [#9017](https://github.com/magento/magento2/issues/9017) -- Duplicate call to $this->getLinkField(); (fixed in [#9057](https://github.com/magento/magento2/pull/9057))
+ * [#7498](https://github.com/magento/magento2/issues/7498) -- Grammar Mistake: You don't subscribe to our newsletter. (fixed in [#9080](https://github.com/magento/magento2/pull/9080))
+ * [#9078](https://github.com/magento/magento2/issues/9078) -- Does not translate "Track All Shipments" in My Account, view order (fixed in [#9095](https://github.com/magento/magento2/pull/9095))
+ * [#5731](https://github.com/magento/magento2/issues/5731) -- FPT label not translatable in the totals on the cart page. (fixed in [#9204](https://github.com/magento/magento2/pull/9204))
+ * [#5040](https://github.com/magento/magento2/issues/5040) -- Change 'select' to 'query' in AbstractSearchResult (fixed in [#5043](https://github.com/magento/magento2/pull/5043))
+ * [#8761](https://github.com/magento/magento2/issues/8761) -- Popup-Modal not closing on Safari/Windows (fixed in [#8824](https://github.com/magento/magento2/pull/8824))
+ * [#7549](https://github.com/magento/magento2/issues/7549) -- Google API Tracking code missing single quote after account number (fixed in [#9084](https://github.com/magento/magento2/pull/9084))
+ * [#1146](https://github.com/magento/magento2/issues/1146) -- A few issues when you use /pub as DocumentRoot (fixed in [#9094](https://github.com/magento/magento2/pull/9094))
+ * [#2802](https://github.com/magento/magento2/issues/2802) -- Sitemap generation in wrong folder when vhost is connected to pub folder (fixed in [#9094](https://github.com/magento/magento2/pull/9094))
+ * [#9200](https://github.com/magento/magento2/issues/9200) -- Create new CLI command: Enable DB logging (fixed in [#9264](https://github.com/magento/magento2/pull/9264))
+ * [#9199](https://github.com/magento/magento2/issues/9199) -- Create New CLI command: Generate Varnish VCL file (fixed in [#9286](https://github.com/magento/magento2/pull/9286))
+ * [#6822](https://github.com/magento/magento2/issues/6822) -- CSS Minify Option Breaks inline SVG XML (fixed in [#9027](https://github.com/magento/magento2/pull/9027))
+ * [#8552](https://github.com/magento/magento2/issues/8552) -- CSS Minifying not compatible with CSS3 calc() function (fixed in [#9027](https://github.com/magento/magento2/pull/9027))
+ * [#9236](https://github.com/magento/magento2/issues/9236) -- Upgrade ZF components. Zend_Json (fixed in [#9262](https://github.com/magento/magento2/pull/9262) and [#9261](https://github.com/magento/magento2/pull/9261) and [#9344](https://github.com/magento/magento2/pull/9344) and [#9753](https://github.com/magento/magento2/pull/9753) and [#9754](https://github.com/magento/magento2/pull/9754))
+ * [#7523](https://github.com/magento/magento2/issues/7523) -- Zend_Db_Statement_Exception after refreshing browser with empty category (fixed in [#9400](https://github.com/magento/magento2/pull/9400))
+ * [#9237](https://github.com/magento/magento2/issues/9237) -- Upgrade ZF components. Zend_Log (fixed in [#9285](https://github.com/magento/magento2/pull/9285))
+ * [#9518](https://github.com/magento/magento2/issues/9518) -- Chrome version 58 causes problems with selections in the tinymce editor (fixed in [#9540](https://github.com/magento/magento2/pull/9540))
+ * [#9241](https://github.com/magento/magento2/issues/9241) -- Upgrade ZF components. Zend_Wildfire (fixed in [#9622](https://github.com/magento/magento2/pull/9622))
+ * [#9239](https://github.com/magento/magento2/issues/9239) -- Upgrade ZF components. Zend_Controller (fixed in [#9622](https://github.com/magento/magento2/pull/9622))
+ * [#6455](https://github.com/magento/magento2/issues/6455) -- Cookie Restriction Mode Overlay is cached by Varnish (fixed in [#9711](https://github.com/magento/magento2/pull/9711))
+ * [#5596](https://github.com/magento/magento2/issues/5596) -- Google Universal Analytics does not track when Cookie Restriction is enabled (fixed in [#9713](https://github.com/magento/magento2/pull/9713))
+ * [#6441](https://github.com/magento/magento2/issues/6441) -- Google Analytics Tracking Code cached by Varnish if Cookie Restriction Settings are active (fixed in [#9713](https://github.com/magento/magento2/pull/9713))
+ * [#9242](https://github.com/magento/magento2/issues/9242) -- Upgrade ZF components. Zend_Session (fixed in [#9348](https://github.com/magento/magento2/pull/9348))
+ * [#7279](https://github.com/magento/magento2/issues/7279) -- Bill-to Name and Ship-to Name trancated to 20 characters in backend (fixed in [#9654](https://github.com/magento/magento2/pull/9654))
+ * [#6151](https://github.com/magento/magento2/issues/6151) -- Can't delete last item in cart if Minimum Order is Enable (fixed in [#9714](https://github.com/magento/magento2/pull/9714))
+ * [#4272](https://github.com/magento/magento2/issues/4272) -- v2.0.4 Credit memos with adjustment fees cannot be fully refunded with a second credit memo (fixed in [#9715](https://github.com/magento/magento2/pull/9715))
+ * [#6207](https://github.com/magento/magento2/issues/6207) -- Checkbox IDs for Terms and Conditions should be unique in Checkout (fixed in [#9717](https://github.com/magento/magento2/pull/9717))
+ * [#7844](https://github.com/magento/magento2/issues/7844) -- Customer with unique attribute can't be saved (fixed in [#9712](https://github.com/magento/magento2/pull/9712))
+ * [#6244](https://github.com/magento/magento2/issues/6244) -- Promo code label not used (fixed in [#9721](https://github.com/magento/magento2/pull/9721))
+ * [#9771](https://github.com/magento/magento2/issues/9771) -- XML instruction referenceBlock does not allow template= or does it? (fixed in [#9772](https://github.com/magento/magento2/pull/9772))
+ * [#8221](https://github.com/magento/magento2/issues/8221) -- Javascript "mixins" doesn't works if 'urlArgs' is in requirejs-config.js (fixed in [#9665](https://github.com/magento/magento2/pull/9665))
+ * [#9278](https://github.com/magento/magento2/issues/9278) -- Create new CLI command: Enable Template Hints (fixed in [#9778](https://github.com/magento/magento2/pull/9778))
+ * [#9819](https://github.com/magento/magento2/issues/9819) -- Authentication_lock settings cannot be edited in the Backend (fixed in [#9820](https://github.com/magento/magento2/pull/9820))
+ * [#9216](https://github.com/magento/magento2/issues/9216) -- Coupon codes not showing in invoice print out (fixed in [#9780](https://github.com/magento/magento2/pull/9780))
+ * [#9679](https://github.com/magento/magento2/issues/9679) -- Translation for layered navigation attribute option not working (fixed in [#9873](https://github.com/magento/magento2/pull/9873))
+ * [#3060](https://github.com/magento/magento2/issues/3060) -- setup:static-content:deploy, setup:di:compile and deploy:mode:set will not return a non-zero exit code if any error occurs (fixed in [#7780](https://github.com/magento/magento2/pull/7780))
+ * [#9421](https://github.com/magento/magento2/issues/9421) -- Inconsistent Gift Options checkbox labels (fixed in [#9525](https://github.com/magento/magento2/pull/9525))
+ * [#9805](https://github.com/magento/magento2/issues/9805) -- Static tests in Windows fail due to file path mismatches (fixed in [#9902](https://github.com/magento/magento2/pull/9902))
+ * [#9924](https://github.com/magento/magento2/issues/9924) -- Prefix and suffix are not prefilled in the quote shipping address (fixed in [#9925](https://github.com/magento/magento2/pull/9925))
+ * [#4237](https://github.com/magento/magento2/issues/4237) -- Cron times in the database have a double timezone correction (fixed in [#9943](https://github.com/magento/magento2/pull/9943))
+ * [#9426](https://github.com/magento/magento2/issues/9426) -- Incorrect order date in Orders grid (fixed in [#9941](https://github.com/magento/magento2/pull/9941))
+ * [#3380](https://github.com/magento/magento2/issues/3380) -- Remove scheduled jobs after changing cron settings (fixed in [#9957](https://github.com/magento/magento2/pull/9957))
+ * [#7504](https://github.com/magento/magento2/issues/7504) -- sitemap image URLs do not match with those on product pages (fixed in [#9082](https://github.com/magento/magento2/pull/9082))
+ * [#8732](https://github.com/magento/magento2/issues/8732) -- The country drop-down list display incorrect after upgrade to 2.1.4 in Admin (fixed in [#9429](https://github.com/magento/magento2/pull/9429))
+ * [#9680](https://github.com/magento/magento2/issues/9680) -- Adding product configurations uses sku based name (fixed in [#9681](https://github.com/magento/magento2/pull/9681))
+ * [#10017](https://github.com/magento/magento2/issues/10017) -- Customer Backend: Confirmation Flag is being overwritten by save of the Customer (fixed in [#9681](https://github.com/magento/magento2/pull/9681))
+ * [#2266](https://github.com/magento/magento2/issues/2266) -- Action column menu does not stay over the action column (fixed in [#10082](https://github.com/magento/magento2/pull/10082))
+ * [#5381](https://github.com/magento/magento2/issues/5381) -- Cannot create a `etc/view.xml` file with an `images` tag with an attribute `module` other than `Magento_Catalog` (fixed in [#10052](https://github.com/magento/magento2/pull/10052))
+ * [#6337](https://github.com/magento/magento2/issues/6337) -- Not able to translate page layout head titles (fixed in [#9992](https://github.com/magento/magento2/pull/9992))
+ * [#3872](https://github.com/magento/magento2/issues/3872) -- Slash as category URL suffix gives 404 error on all category pages (fixed in [#10043](https://github.com/magento/magento2/pull/10043))
+ * [#4660](https://github.com/magento/magento2/issues/4660) -- Multiple URLs causes duplicated content (fixed in [#10043](https://github.com/magento/magento2/pull/10043))
+ * [#4876](https://github.com/magento/magento2/issues/4876) -- Product URL Suffix "/" results in 404 error (fixed in [#10043](https://github.com/magento/magento2/pull/10043))
+ * [#8264](https://github.com/magento/magento2/issues/8264) -- Custom URL Rewrite where the request path ends with a forward slash is not matched (fixed in [#10043](https://github.com/magento/magento2/pull/10043))
+ * [#6396](https://github.com/magento/magento2/issues/6396) -- Cart Price Rules: Category selection UI for Conditions do not come up. (fixed in [#10094](https://github.com/magento/magento2/pull/10094))
+ * [#10124](https://github.com/magento/magento2/issues/10124) -- Wrong order of "width" x "height" when uploading image to admin under Content>Design Config (fixed in [#10126](https://github.com/magento/magento2/pull/10126))
+ * [#6594](https://github.com/magento/magento2/issues/6594) -- Magento 2.1 EE: simplexml_load_string() error in custom widget (fixed in [#10151](https://github.com/magento/magento2/pull/10151))
+ * [#10148](https://github.com/magento/magento2/issues/10148) -- Developer ACL incorrect (fixed in [#10149](https://github.com/magento/magento2/pull/10149))
+ * [#9445](https://github.com/magento/magento2/issues/9445) -- Cart 860 does not contain item 1204 (fixed in [#10059](https://github.com/magento/magento2/pull/10059))
+* GitHub pull requests:
+ * [#4078](https://github.com/magento/magento2/pull/4078) -- Add logo folder to list of allowed resources (by @thaiphan)
+ * [#4355](https://github.com/magento/magento2/pull/4355) -- Enforce password and password-confirm as strings (by @nevvermind)
+ * [#4175](https://github.com/magento/magento2/pull/4175) -- PHP 7 (by @rafaelstz)
+ * [#4669](https://github.com/magento/magento2/pull/4669) -- Php 5.5 support was removed (by @fooman)
+ * [#4766](https://github.com/magento/magento2/pull/4766) -- The time has come for splat operator (by @orlangur)
+ * [#4867](https://github.com/magento/magento2/pull/4867) -- Fix commandExecutor interface mismatch (by @wexo-team)
+ * [#3705](https://github.com/magento/magento2/pull/3705) -- fix issue #3704 regarding integer attribute values being cast to decimal (by @digitalpianism)
+ * [#3750](https://github.com/magento/magento2/pull/3750) -- Add ability to show custom error message on Authorizenet place order (by @ytorbyk)
+ * [#3859](https://github.com/magento/magento2/pull/3859) -- Fixes _toHtml method of Checkout/Block/Cart/Link class (by @ddonnini)
+ * [#4017](https://github.com/magento/magento2/pull/4017) -- Add the $t to translate the message (by @mamzellejuu)
+ * [#4239](https://github.com/magento/magento2/pull/4239) -- Attribute Model specifiable in Propertymapper (by @liolemaire)
+ * [#4583](https://github.com/magento/magento2/pull/4583) -- Fix desktop spelling in lib/web/css #4557 (by @BenSpace48)
+ * [#2735](https://github.com/magento/magento2/pull/2735) -- Add database port to Magento Setup Model Installer (by @tkn98)
+ * [#4275](https://github.com/magento/magento2/pull/4275) -- Database port missing in Magento\TestFramework\Db\Mysql #3529 (by @gordonknoppe)
+ * [#4372](https://github.com/magento/magento2/pull/4372) -- Remove unused property + use ::class (by @nevvermind)
+ * [#2274](https://github.com/magento/magento2/pull/2274) -- TypeError: this.trigger is not a function (by @daim2k5)
+ * [#3050](https://github.com/magento/magento2/pull/3050) -- Replace PrototypeJS (by @srenon)
+ * [#3436](https://github.com/magento/magento2/pull/3436) -- Fix stripped-min-length check (by @avoelkl)
+ * [#3638](https://github.com/magento/magento2/pull/3638) -- Fix #3637 Add missing catalogsearch layout for swatches. (by @romainruaud)
+ * [#3633](https://github.com/magento/magento2/pull/3633) -- #768 - fix Missing acl.xml for the Magento_Checkout module (by @tzyganu)
+ * [#3708](https://github.com/magento/magento2/pull/3708) -- Add 'yyyy' to nomalizedDate method map for adminhtml i18n (by @Amakata)
+ * [#3973](https://github.com/magento/magento2/pull/3973) -- fix bug on newsletter subscription action when user use an existing email already subscribed (by @manfrinm)
+ * [#3932](https://github.com/magento/magento2/pull/3932) -- Update Date.php (by @Maddesto)
+ * [#3907](https://github.com/magento/magento2/pull/3907) -- Update Layout.php (by @Maddesto)
+ * [#4051](https://github.com/magento/magento2/pull/4051) -- Fix "minimum-length" option of the "validate-length" JS validation (by @adragus-inviqa)
+ * [#4269](https://github.com/magento/magento2/pull/4269) -- ConfigurableProduct validator, first check if array item exists (by @BlackIkeEagle)
+ * [#4496](https://github.com/magento/magento2/pull/4496) -- Access element through jQuery (by @sikker)
+ * [#4631](https://github.com/magento/magento2/pull/4631) -- Previous is spelt incorrectly (by @BenSpace48)
+ * [#4882](https://github.com/magento/magento2/pull/4882) -- Fix handling is_region_required key as optional (by @komsitr)
+ * [#5028](https://github.com/magento/magento2/pull/5028) -- Load jquery using requirejs to print page (by @Bartlomiejsz)
+ * [#1957](https://github.com/magento/magento2/pull/1957) -- Add required interface implementation (by @udovicic)
+ * [#2492](https://github.com/magento/magento2/pull/2492) -- No translation from "Orders and Returns" footer links (by @mageho)
+ * [#3749](https://github.com/magento/magento2/pull/3749) -- Stop screen loader on payment error (by @ytorbyk)
+ * [#4901](https://github.com/magento/magento2/pull/4901) -- Fix XML validation value type of post-code to "boolean" (by @adragus-inviqa)
+ * [#5066](https://github.com/magento/magento2/pull/5066) -- Add underscore as dependency to quote.js (by @Bartlomiejsz)
+ * [#5116](https://github.com/magento/magento2/pull/5116) -- Fixes invalid json objects in the order email templates. (by @hostep)
+ * [#5095](https://github.com/magento/magento2/pull/5095) -- Fix integration test expectation to match count of countries in the database (by @Vinai)
+ * [#5200](https://github.com/magento/magento2/pull/5200) -- Adminhtml sales view - Escape remoteIp (by @convenient)
+ * [#4294](https://github.com/magento/magento2/pull/4294) -- Static deploy flags (by @denisristic)
+ * [#3137](https://github.com/magento/magento2/pull/3137) -- Update nginx.conf.sample (by @thaiphan)
+ * [#3746](https://github.com/magento/magento2/pull/3746) -- Add HttpInterface methods and add up-casts for type safety (by @Vinai)
+ * [#4354](https://github.com/magento/magento2/pull/4354) -- Adds test for getDataUsingMethod using digits (by @fooman)
+ * [#4396](https://github.com/magento/magento2/pull/4396) -- Don't hardcode the Magento_Backend::admin index (by @annybs)
+ * [#4491](https://github.com/magento/magento2/pull/4491) -- Correction: variable naming for user roles & role id (by @MagePsycho)
+ * [#4519](https://github.com/magento/magento2/pull/4519) -- Create auth.json.sample (by @rafaelstz)
+ * [#3688](https://github.com/magento/magento2/pull/3688) -- added partial fix for issue #2617. (by @whizkid79)
+ * [#3770](https://github.com/magento/magento2/pull/3770) -- Bugfix: Unable to activate search form on phone (by @vovayatsyuk)
+ * [#4011](https://github.com/magento/magento2/pull/4011) -- Fixed post var name for update attributes (by @Corefix)
+ * [#2791](https://github.com/magento/magento2/pull/2791) -- Update Template.php (by @liam-wiltshire)
+ * [#5496](https://github.com/magento/magento2/pull/5496) -- Fixes #5495 (Mobile navigation submenus need two clicks to open) (by @ajpevers)
+ * [#5725](https://github.com/magento/magento2/pull/5725) -- Fix/translation in validation files (by @dvynograd)
+ * [#5915](https://github.com/magento/magento2/pull/5915) -- Added missing translation to range grid filter (by @maqlec)
+ * [#5884](https://github.com/magento/magento2/pull/5884) -- Use alias already defined in requirejs-config.js (by @kassner)
+ * [#4388](https://github.com/magento/magento2/pull/4388) -- Fixed column description for "website_id" column (by @ikk0)
+ * [#1628](https://github.com/magento/magento2/pull/1628) -- Add cache of configuration files list (by @otakarmare)
+ * [#4791](https://github.com/magento/magento2/pull/4791) -- Replace fabpot/php-cs-fixer with friendsofphp/php-cs-fixer (by @GordonLesti)
+ * [#5983](https://github.com/magento/magento2/pull/5983) -- Fix 'Track your order' i18n. (by @peec)
+ * [#1988](https://github.com/magento/magento2/pull/1988) -- Use inline elements for inline links (by @chicgeek)
+ * [#6283](https://github.com/magento/magento2/pull/6283) -- Update elements.xsd (by @aholovan)
+ * [#1935](https://github.com/magento/magento2/pull/1935) -- Added 'target' attribute to the allowed attributes array for link block (by @fcapua-summa)
+ * [#4733](https://github.com/magento/magento2/pull/4733) -- Escape Js Quote for layout updates (by @bchatard)
+ * [#4565](https://github.com/magento/magento2/pull/4565) -- Update Container.php (by @Maddesto)
+ * [#4845](https://github.com/magento/magento2/pull/4845) -- Set return code for SetModeCommand (by @mc388)
+ * [#2037](https://github.com/magento/magento2/pull/2037) -- Correct the error message when creating wrong object for block (by @hiephm)
+ * [#3779](https://github.com/magento/magento2/pull/3779) -- Add dispatching of view_block_abstract_to_html_after event (by @aleron75)
+ * [#5741](https://github.com/magento/magento2/pull/5741) -- Update AbstractTemplate.php (by @vivek201)
+ * [#5145](https://github.com/magento/magento2/pull/5145) -- \Magento\CatalogInventory\Model\Stock\Status->getStockId() to return correct value (by @fe-lix-)
+ * [#6009](https://github.com/magento/magento2/pull/6009) -- Update README.md (by @fooman)
+ * [#6280](https://github.com/magento/magento2/pull/6280) -- Fixed layout for customer authentication popup (by @rogyar)
+ * [#6549](https://github.com/magento/magento2/pull/6549) -- Remove obsolete comment in catalog_category_view.xml (by @pdanzinger)
+ * [#6804](https://github.com/magento/magento2/pull/6804) -- Fix nav not working in mobile (by @slackerzz)
+ * [#6801](https://github.com/magento/magento2/pull/6801) -- Fixed unclosed span tag in Review module (by @PingusPepan)
+ * [#5045](https://github.com/magento/magento2/pull/5045) -- Fix runner for JsTestDriver based tests on OS X (by @Vinai)
+ * [#4958](https://github.com/magento/magento2/pull/4958) -- block newsletter title (by @slackerzz)
+ * [#5548](https://github.com/magento/magento2/pull/5548) -- Fix error with the WYSIWYG and Greek characters (by @sakisplus)
+ * [#7256](https://github.com/magento/magento2/pull/7256) -- Fix incorrect table name during catalog product indexing (by @nagno)
+ * [#6972](https://github.com/magento/magento2/pull/6972) -- Fix issue #6968 since checkout success title is not displayed (by @AngelVazquezArroyo)
+ * [#4121](https://github.com/magento/magento2/pull/4121) -- Ensure composer.json exists (by @dank00)
+ * [#4134](https://github.com/magento/magento2/pull/4134) -- Added call to action to compile command error (by @sammarcus)
+ * [#6974](https://github.com/magento/magento2/pull/6974) -- Fixed calling non exists order address->getCountry to getCountryId (by @magexo)
+ * [#4088](https://github.com/magento/magento2/pull/4088) -- Fix newsletter queue subscribers adding performance (by @DariuszMaciejewski)
+ * [#6952](https://github.com/magento/magento2/pull/6952) -- Update AccountLock.php (by @klict)
+ * [#5465](https://github.com/magento/magento2/pull/5465) -- Fix Magento\Review\Model\ResourceModel\Rating\Option not instantiable in setup scripts (by @adragus-inviqa)
+ * [#7799](https://github.com/magento/magento2/pull/7799) -- Remove duplicate code from template file. (by @dverkade)
+ * [#7919](https://github.com/magento/magento2/pull/7919) -- Using Dynamic Protocol Concatination (by @brobie)
+ * [#7921](https://github.com/magento/magento2/pull/7921) -- Removed un-used static version rewrite rule in nginx.conf.sample (by @careys7)
+ * [#8019](https://github.com/magento/magento2/pull/8019) -- added fr_CH to allowed locales list (by @annapivniak)
+ * [#8082](https://github.com/magento/magento2/pull/8082) -- issue 8080: Cron configuration loading from DB doesn't work (by @ytorbyk)
+ * [#8062](https://github.com/magento/magento2/pull/8062) -- Fix theme source model used in widget grid (by @Zefiryn)
+ * [#8232](https://github.com/magento/magento2/pull/8232) -- Fix notice during DI compilation (by @qrz-io)
+ * [#8306](https://github.com/magento/magento2/pull/8306) -- Remove warning from setup/src/Magento/Setup/Module/I18n/Dictionary/Writer/Csv.php (by @dmanners)
+ * [#8185](https://github.com/magento/magento2/pull/8185) -- Fix #7461 using the simplest approach for now (by @lazyguru)
+ * [#8183](https://github.com/magento/magento2/pull/8183) -- [BUGFIX] Fixed the credit memo guest email (by @nickgraz)
+ * [#8161](https://github.com/magento/magento2/pull/8161) -- Return focus to search input after closing autocomplete dropdown (by @evktalo)
+ * [#8155](https://github.com/magento/magento2/pull/8155) -- Correct php doc (by @angelomaragna)
+ * [#8341](https://github.com/magento/magento2/pull/8341) -- Use better function for 'Continue Shopping' url (by @dmatthew)
+ * [#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)
+ * [#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)
+ * [#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)
+ * [#8416](https://github.com/magento/magento2/pull/8416) -- Use configured product attributes in wishlist item collection (by @schmengler)
+ * [#8414](https://github.com/magento/magento2/pull/8414) -- Replace toGMTString with toUTCString (by @schmengler)
+ * [#8419](https://github.com/magento/magento2/pull/8419) -- Use page result instead of rendering layout directly in controllers (by @schmengler)
+ * [#8331](https://github.com/magento/magento2/pull/8331) -- Remove Zend1 Json from Magento Captcha module (by @dmanners)
+ * [#8252](https://github.com/magento/magento2/pull/8252) -- Customer account edit form: additional info block visible (by @Sylvco)
+ * [#8405](https://github.com/magento/magento2/pull/8405) -- Remove the unused Ajax/Serializer.php class (by @dmanners)
+ * [#8402](https://github.com/magento/magento2/pull/8402) -- Remove Zend1 db from captcha module (by @dmanners)
+ * [#8463](https://github.com/magento/magento2/pull/8463) -- Remove outdated comment (by @evktalo)
+ * [#8456](https://github.com/magento/magento2/pull/8456) -- Specifically ask for the Json Serializer object Mage Braintree (by @dmanners)
+ * [#8446](https://github.com/magento/magento2/pull/8446) -- Fix "each()" function call on potentially invalid data (by @giacmir)
+ * [#5928](https://github.com/magento/magento2/pull/5928) -- prevent double shipping method selection (by @danslo)
+ * [#8217](https://github.com/magento/magento2/pull/8217) -- Update DataProvider.php (by @redelschaap)
+ * [#8413](https://github.com/magento/magento2/pull/8413) -- Load translations for area, fixes #8412 (by @fooman)
+ * [#8356](https://github.com/magento/magento2/pull/8356) -- Remove Zend1 captcha from Magento2 Captcha module (by @dmanners)
+ * [#8474](https://github.com/magento/magento2/pull/8474) -- Fix for https://github.com/magento/magento2/issues/8287 (by @ericrisler)
+ * [#8487](https://github.com/magento/magento2/pull/8487) -- Mark the STDO test as skipped (by @dmanners)
+ * [#3155](https://github.com/magento/magento2/pull/3155) -- Remove unused variables in unit test (by @fooman)
+ * [#6049](https://github.com/magento/magento2/pull/6049) -- Added typo correction for table name comment (by @atishgoswami)
+ * [#4370](https://github.com/magento/magento2/pull/4370) -- update Catalog Helper (by @barbarich-p)
+ * [#4106](https://github.com/magento/magento2/pull/4106) -- Adds support for purging varnish cache based on an X-Pool header (by @davidalger)
+ * [#7982](https://github.com/magento/magento2/pull/7982) -- Add bin/magento commands to list store/website data (by @convenient)
+ * [#8434](https://github.com/magento/magento2/pull/8434) -- Problem on mobile when catalog gallery allowfullscreen is false #5808 (by @Crossmotion)
+ * [#8417](https://github.com/magento/magento2/pull/8417) -- add anonymize ip option for google analytics (by @thomas-villagers)
+ * [#8498](https://github.com/magento/magento2/pull/8498) -- Product->save() shouldn't be called directly (by @stansm)
+ * [#8581](https://github.com/magento/magento2/pull/8581) -- Fixed overly bold icons in Firefox Mac (by @TandyCorp)
+ * [#8481](https://github.com/magento/magento2/pull/8481) -- Remove zend json checkout (by @dmanners)
+ * [#8518](https://github.com/magento/magento2/pull/8518) -- Change link text from "Report Bugs" to "Report a Bug" (by @Zifius)
+ * [#8514](https://github.com/magento/magento2/pull/8514) -- Add missing name attributes to catalog_product_view layout xml (by @andrewnoble)
+ * [#8505](https://github.com/magento/magento2/pull/8505) -- Update the Adminhtml image tree JSON (by @dmanners)
+ * [#8467](https://github.com/magento/magento2/pull/8467) -- Fix for magento/magento2#8392 (by @kirashet666)
+ * [#8617](https://github.com/magento/magento2/pull/8617) -- Remove Zend_Json from Customer module (by @dmanners)
+ * [#8609](https://github.com/magento/magento2/pull/8609) -- [PHP 7.1 Compatibility] Void became a reserved word (by @orlangur)
+ * [#8611](https://github.com/magento/magento2/pull/8611) -- Fix #8308: test setup/src/Magento/Setup/Test/Unit/Model/Cron/JobSetCacheTest.php crashes in debug mode (by @orlangur)
+ * [#8610](https://github.com/magento/magento2/pull/8610) -- Fix #8315: test setup/src/Magento/Setup/Test/Unit/Module/I18n/Dictionary/Writer/Csv/StdoTest.php crashes in debug mode (by @orlangur)
+ * [#7339](https://github.com/magento/magento2/pull/7339) -- Fix/xml parser issue (by @dvynograd)
+ * [#7345](https://github.com/magento/magento2/pull/7345) -- Fix typo (by @mpchadwick)
+ * [#7406](https://github.com/magento/magento2/pull/7406) -- MAGETWO-60448 (by @vasilii-b)
+ * [#8000](https://github.com/magento/magento2/pull/8000) -- Set static html fragments as cacheable (by @paveq)
+ * [#8077](https://github.com/magento/magento2/pull/8077) -- Type cast (by @deriknel)
+ * [#8278](https://github.com/magento/magento2/pull/8278) -- bug #8277 fixing bug with https downloading. (by @clementbeudot)
+ * [#8420](https://github.com/magento/magento2/pull/8420) -- Refactor contact module (by @schmengler)
+ * [#8519](https://github.com/magento/magento2/pull/8519) -- Make "is_required" and "is_visible" properties of telephone, company and fax attributes of addresses configurable (by @avstudnitz)
+ * [#8537](https://github.com/magento/magento2/pull/8537) -- Fixed missing echo statement in checkout cart coupon template (by @FrankRuis)
+ * [#8602](https://github.com/magento/magento2/pull/8602) -- Fixed crosssells count always null (by @koenner01)
+ * [#8593](https://github.com/magento/magento2/pull/8593) -- Fix #7371 (by @rossluk)
+ * [#8642](https://github.com/magento/magento2/pull/8642) -- Update Save.php (by @josefbehr)
+ * [#8633](https://github.com/magento/magento2/pull/8633) -- Fix #8632 (by @koenner01)
+ * [#8513](https://github.com/magento/magento2/pull/8513) -- Replace ext name spaces with dashes (as composer does when storing) (by @AydinHassan)
+ * [#8668](https://github.com/magento/magento2/pull/8668) -- Add correct return type in order service (by @cmuench)
+ * [#8677](https://github.com/magento/magento2/pull/8677) -- Fixed Doc Block for the dispatch method of the Rest Controller (by @vrann)
+ * [#3585](https://github.com/magento/magento2/pull/3585) -- Remove duplicate switchIsFilterable (by @GordonLesti)
+ * [#8132](https://github.com/magento/magento2/pull/8132) -- Fix incorrect schema definition for price (by @unfunco)
+ * [#2275](https://github.com/magento/magento2/pull/2275) -- Updated links to point to http://devdocs.magento.com/guides/v2.0 (by @wjarka)
+ * [#8683](https://github.com/magento/magento2/pull/8683) -- Fixed return type of OrderRepository::getList (by @clementbeudot)
+ * [#8682](https://github.com/magento/magento2/pull/8682) -- Remove unused argument (by @mfdj)
+ * [#2185](https://github.com/magento/magento2/pull/2185) -- unused variable (by @barbarich-p)
+ * [#7894](https://github.com/magento/magento2/pull/7894) -- Fix #7893 (by @andreaspenz)
+ * [#8623](https://github.com/magento/magento2/pull/8623) -- Fix check for boolean product attributes (by @TKlement)
+ * [#8678](https://github.com/magento/magento2/pull/8678) -- Fixed Issue #8425 (by @DavidLambauer)
+ * [#8711](https://github.com/magento/magento2/pull/8711) -- Remove Zend_Json from the persistent module remember me status observer (by @dmanners)
+ * [#8706](https://github.com/magento/magento2/pull/8706) -- Remove Zend_Json from the unit test in the CustomerImportExport module (by @dmanners)
+ * [#8723](https://github.com/magento/magento2/pull/8723) -- Add active class to search form wrapper for more theming flexibility (by @andrewkett)
+ * [#8768](https://github.com/magento/magento2/pull/8768) -- Fix issue #8709 (by @renatocason)
+ * [#8762](https://github.com/magento/magento2/pull/8762) -- Fix issue #2558 (by @renatocason)
+ * [#8759](https://github.com/magento/magento2/pull/8759) -- magento/magento2#8618: Apply coupon code button issue on Checkout (by @mcspronko)
+ * [#8776](https://github.com/magento/magento2/pull/8776) -- Range filter doesn't works with 0 values in admin #7103 (by @giacmir)
+ * [#7611](https://github.com/magento/magento2/pull/7611) -- Update catalog_rule_form.xml edit labels (by @fernandofauth)
+ * [#7650](https://github.com/magento/magento2/pull/7650) -- Add host header to varnish cache purge request (by @m0zziter)
+ * [#7914](https://github.com/magento/magento2/pull/7914) -- FPC JS - Fix for CORS issue (by @OZZlE)
+ * [#8013](https://github.com/magento/magento2/pull/8013) -- Update resets.html (by @ryantfowler)
+ * [#8041](https://github.com/magento/magento2/pull/8041) -- Fix USPS Priority Mail to Canada (by @jaywilliams)
+ * [#8014](https://github.com/magento/magento2/pull/8014) -- Update _resets.less (by @ryantfowler)
+ * [#8053](https://github.com/magento/magento2/pull/8053) -- Strict checking types during di compilation (by @michalderlatka)
+ * [#8048](https://github.com/magento/magento2/pull/8048) -- Consistent HTML tags and breaks (by @chickenland)
+ * [#8056](https://github.com/magento/magento2/pull/8056) -- update allowed container tags (by @steros)
+ * [#8158](https://github.com/magento/magento2/pull/8158) -- Fix OAuth request helper to support Authorization header value parsing with non-leading OAuth key (by @careys7)
+ * [#8589](https://github.com/magento/magento2/pull/8589) -- Set correct primary keys for temporary tables in product flat indexer (by @jarnooravainen)
+ * [#8736](https://github.com/magento/magento2/pull/8736) -- Update Stock.php (by @Corefix)
+ * [#8119](https://github.com/magento/magento2/pull/8119) -- Throw exception for invalid (missing) template in dev mode (by @convenient)
+ * [#8690](https://github.com/magento/magento2/pull/8690) -- Fix SALES_ORDER_TAX_ITEM_TAX_ID_ITEM_ID duplicates (by @mimarcel)
+ * [#8743](https://github.com/magento/magento2/pull/8743) -- Validate PHP classnames in di.xml files via schema (by @ktomk)
+ * [#8714](https://github.com/magento/magento2/pull/8714) -- Quote values in IN() predicate to avoid cast issues (by @xi-ao)
+ * [#8835](https://github.com/magento/magento2/pull/8835) -- Replace Zend_Json from the Magento Review module (by @dmanners)
+ * [#8832](https://github.com/magento/magento2/pull/8832) -- Replace Zend_Json from the Magento Quote module (by @dmanners)
+ * [#8839](https://github.com/magento/magento2/pull/8839) -- Rename to DataObjectTest (by @mfdj)
+ * [#2199](https://github.com/magento/magento2/pull/2199) -- Rename admin sidebar Products to Catalog #2060 (by @markoshust)
+ * [#5620](https://github.com/magento/magento2/pull/5620) -- Remove superfluous method call. (by @maksim-grib)
+ * [#6767](https://github.com/magento/magento2/pull/6767) -- Removed setting routerlist twice. (by @dverkade)
+ * [#6257](https://github.com/magento/magento2/pull/6257) -- Fix "none" is not meant to be translated in catalog_product_view layout file (by @azanelli)
+ * [#7645](https://github.com/magento/magento2/pull/7645) -- Storecode in url changed from default to all (by @bartlubbersen)
+ * [#7864](https://github.com/magento/magento2/pull/7864) -- Bugfix: Fix for not respected alternative headers in maintenance mode (by @cmuench)
+ * [#7794](https://github.com/magento/magento2/pull/7794) -- Check return value for getProduct() in getPrice(). (by @pbaylies)
+ * [#8052](https://github.com/magento/magento2/pull/8052) -- Update PurgeCache.php (by @bery)
+ * [#8130](https://github.com/magento/magento2/pull/8130) -- Update Options.php (by @redelschaap)
+ * [#8079](https://github.com/magento/magento2/pull/8079) -- FIX Backend mass delete Sql error (by @asubit)
+ * [#7234](https://github.com/magento/magento2/pull/7234) -- Lossless images optimalization (by @Igloczek)
+ * [#8685](https://github.com/magento/magento2/pull/8685) -- [PSR-2 Compliance] Fix #8612: Hundreds of PHPCS-based static tests violations in mainline (by @orlangur)
+ * [#7568](https://github.com/magento/magento2/pull/7568) -- Minor Update Mysql.php (by @WJdeBaas)
+ * [#7598](https://github.com/magento/magento2/pull/7598) -- Bugfix for _getProductCollection on a product page (by @evgk)
+ * [#8886](https://github.com/magento/magento2/pull/8886) -- Fix typo in app/code/Magento/Email/Test/Unit/Model/TemplateTest.php (by @orlangur)
+ * [#5029](https://github.com/magento/magento2/pull/5029) -- Add default swatch_image placeholder (by @Bartlomiejsz)
+ * [#6838](https://github.com/magento/magento2/pull/6838) -- Removed preference from di.xml (by @dverkade)
+ * [#7578](https://github.com/magento/magento2/pull/7578) -- Don't skip attribute options with a value of 0 from the layered navigation (by @dcabrejas)
+ * [#7615](https://github.com/magento/magento2/pull/7615) -- Fixes duplicate messages shown when adding items to cart (by @comdiler)
+ * [#7590](https://github.com/magento/magento2/pull/7590) -- Reset skippedRows array in clear method (by @ccasciotti)
+ * [#7701](https://github.com/magento/magento2/pull/7701) -- Allows you to have 0 as a option (by @Corefix)
+ * [#7748](https://github.com/magento/magento2/pull/7748) -- Remove _required class from ZIP field (by @dmitryshkolnikov)
+ * [#7743](https://github.com/magento/magento2/pull/7743) -- MAGETWO-61828: Text swatch "zero" not shown (by @oroskodias)
+ * [#8883](https://github.com/magento/magento2/pull/8883) -- Fix a typo (by @evgk)
+ * [#7541](https://github.com/magento/magento2/pull/7541) -- Added "Add / update" comment. Update was missing. (by @gastondisacco)
+ * [#8808](https://github.com/magento/magento2/pull/8808) -- Change link text from "Report a Bug" to "Report an Issue" (by @Zifius)
+ * [#8907](https://github.com/magento/magento2/pull/8907) -- Removed unused dependencys (by @ikrs)
+ * [#8910](https://github.com/magento/magento2/pull/8910) -- Prevent overwriting grunt config (by @Igloczek)
+ * [#7562](https://github.com/magento/magento2/pull/7562) -- Update AbstractCart.php (by @Corefix)
+ * [#8766](https://github.com/magento/magento2/pull/8766) -- fix $childrenWrapClass never used (by @slackerzz)
+ * [#8822](https://github.com/magento/magento2/pull/8822) -- Upgrade PHP CS Fixer to v2 (by @keradus)
+ * [#8901](https://github.com/magento/magento2/pull/8901) -- Update design_config_form.xml (by @WaPoNe)
+ * [#8896](https://github.com/magento/magento2/pull/8896) -- Fix all words without " (by @rafaelstz)
+ * [#8912](https://github.com/magento/magento2/pull/8912) -- magento/magento2#8590: M2.1.4 : ArrayBackend cannot save and Added country regions for Croatia (by @nkajic)
+ * [#8909](https://github.com/magento/magento2/pull/8909) -- magento/magento2#8863: Malta zipcode validation incomplete (by @mmacinko)
+ * [#2829](https://github.com/magento/magento2/pull/2829) -- Update final_price.phtml (by @liam-wiltshire)
+ * [#3869](https://github.com/magento/magento2/pull/3869) -- Optional PHP7.0 Socket Path (by @digimix)
+ * [#4854](https://github.com/magento/magento2/pull/4854) -- Fix admin menu bug (by @NikolasSumrak)
+ * [#6400](https://github.com/magento/magento2/pull/6400) -- Fix for Product Attribute's Conditions (by @NikolasSumrak)
+ * [#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)
+ * [#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)
+ * [#6794](https://github.com/magento/magento2/pull/6794) -- Move cache to instance of price box widget (by @JamesonNetworks)
+ * [#6856](https://github.com/magento/magento2/pull/6856) -- Issue/6855 (by @PascalBrouwers)
+ * [#6837](https://github.com/magento/magento2/pull/6837) -- Removed argument from di.xml (by @dverkade)
+ * [#6878](https://github.com/magento/magento2/pull/6878) -- Media attribute folder to be ignored (by @rafaelstz)
+ * [#6914](https://github.com/magento/magento2/pull/6914) -- Put the contants in the same docblock together (by @dverkade)
+ * [#6913](https://github.com/magento/magento2/pull/6913) -- Removed commented out line for ignoring coding standards (by @dverkade)
+ * [#7142](https://github.com/magento/magento2/pull/7142) -- [Framework][Translate] Changed translation order to allow language (by @JSchlarb)
+ * [#7352](https://github.com/magento/magento2/pull/7352) -- Product export duplicate rows for product with html special chars in data. Bugfix for #7350 (by @comdiler)
+ * [#8445](https://github.com/magento/magento2/pull/8445) -- Changed name of dispatched object in delete event (by @clementbeudot)
+ * [#8959](https://github.com/magento/magento2/pull/8959) -- Fixes #2461 (by @ajpevers)
+ * [#8950](https://github.com/magento/magento2/pull/8950) -- Remove deadcode (by @jipjop)
+ * [#3469](https://github.com/magento/magento2/pull/3469) -- respect depends declaration in system.xml for form element (by @phoenix-bjoern)
+ * [#5362](https://github.com/magento/magento2/pull/5362) -- Make CMS directive filtering error be more generic (by @erikhansen)
+ * [#6354](https://github.com/magento/magento2/pull/6354) -- Newsletter Email fix (by @inettman)
+ * [#6744](https://github.com/magento/magento2/pull/6744) -- add-product-image-label-bug (by @markpol)
+ * [#6773](https://github.com/magento/magento2/pull/6773) -- Allow to insert Persian characters for attributes! (by @sIiiS)
+ * [#6779](https://github.com/magento/magento2/pull/6779) -- Changed documentation of the cache variable (by @dverkade)
+ * [#6775](https://github.com/magento/magento2/pull/6775) -- Changed constructor to use interface instead of direct classname (by @dverkade)
+ * [#6971](https://github.com/magento/magento2/pull/6971) -- fix #6961 (by @razbakov)
+ * [#7097](https://github.com/magento/magento2/pull/7097) -- You -> Your (by @avitex)
+ * [#7414](https://github.com/magento/magento2/pull/7414) -- Fix typo (by @convenient)
+ * [#8005](https://github.com/magento/magento2/pull/8005) -- Prevent cross origin iframe content reading (by @Igloczek)
+ * [#8753](https://github.com/magento/magento2/pull/8753) -- Added Translation for required Data-Attribute (by @DavidLambauer)
+ * [#8778](https://github.com/magento/magento2/pull/8778) -- magento/magento2: #8765 (by @cavalier79)
+ * [#8914](https://github.com/magento/magento2/pull/8914) -- magento/magetno2#8529:Typo in error message "Table is not exists" (by @bvrbanec)
+ * [#2448](https://github.com/magento/magento2/pull/2448) -- Fix Inconsistency (by @srenon)
+ * [#2093](https://github.com/magento/magento2/pull/2093) -- Remove call to load() in getChildrenCategories method (by @davidalger)
+ * [#4179](https://github.com/magento/magento2/pull/4179) -- fix typo in Magento_Catalog toolbar less source (by @gil--)
+ * [#5078](https://github.com/magento/magento2/pull/5078) -- Rename $websiteId to $scopeId (by @flancer64)
+ * [#5207](https://github.com/magento/magento2/pull/5207) -- Table name fix - rule_customer to salesrule_customer (by @Bartlomiejsz)
+ * [#5858](https://github.com/magento/magento2/pull/5858) -- Save shipping discount in $total (by @flancer64)
+ * [#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)
+ * [#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)
+ * [#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)
+ * [#8994](https://github.com/magento/magento2/pull/8994) -- Syntax fix (by @rafaelstz)
+ * [#1895](https://github.com/magento/magento2/pull/1895) -- Fix relative template references in individual Magento modules (by @davidalger)
+ * [#4224](https://github.com/magento/magento2/pull/4224) -- Update get.php (by @thaiphan)
+ * [#6567](https://github.com/magento/magento2/pull/6567) -- Always skip hidden files (by @quickshiftin)
+ * [#6989](https://github.com/magento/magento2/pull/6989) -- Allow extending config variables (by @adragus-inviqa)
+ * [#7218](https://github.com/magento/magento2/pull/7218) -- Set store id on block only when empty (by @dank00)
+ * [#7161](https://github.com/magento/magento2/pull/7161) -- Fix a bug resulting in incorrect offsets with dynamic row drag-n-drop functionality (by @navarr)
+ * [#7664](https://github.com/magento/magento2/pull/7664) -- Fix for "Stock Status" field not disappearing in admin when "Manage Stock" = No (by @comdiler)
+ * [#8812](https://github.com/magento/magento2/pull/8812) -- Fix count SQL on products sold collection (by @jameshalsall)
+ * [#8991](https://github.com/magento/magento2/pull/8991) -- Update AbstractModel.php (by @redelschaap)
+ * [#9001](https://github.com/magento/magento2/pull/9001) -- Fix #3791 (by @quienti)
+ * [#8998](https://github.com/magento/magento2/pull/8998) -- Fixed empty submenu group in backend menu (by @vovayatsyuk)
+ * [#8928](https://github.com/magento/magento2/pull/8928) -- Stop $this->validColumnNames array from growing and growing (by @jalogut)
+ * [#4149](https://github.com/magento/magento2/pull/4149) -- Fix wording of downloadable Products (by @bh-ref)
+ * [#4674](https://github.com/magento/magento2/pull/4674) -- Refactor repeating logic (by @nevvermind)
+ * [#4501](https://github.com/magento/magento2/pull/4501) -- Add Crowdin badge - official translations (by @piotrekkaminski)
+ * [#6243](https://github.com/magento/magento2/pull/6243) -- Set getConnection() as public method (by @flancer64)
+ * [#6250](https://github.com/magento/magento2/pull/6250) -- Return the same data on exit (by @flancer64)
+ * [#4844](https://github.com/magento/magento2/pull/4844) -- Fix typo (by @orlangur)
+ * [#4874](https://github.com/magento/magento2/pull/4874) -- Make NL zipcode pattern less strict (by @tdgroot)
+ * [#5400](https://github.com/magento/magento2/pull/5400) -- Privacy Policy translation (fixes Issue #2951) (by @MindConflicts)
+ * [#5671](https://github.com/magento/magento2/pull/5671) -- Fix small typo (by @adragus-inviqa)
+ * [#6132](https://github.com/magento/magento2/pull/6132) -- Remove an extra space while clearing indexed stock items. (by @nntoan)
+ * [#6840](https://github.com/magento/magento2/pull/6840) -- Removed unused variable $routerId (by @dverkade)
+ * [#6834](https://github.com/magento/magento2/pull/6834) -- Removed default values for title, meta description, better labels. (by @paales)
+ * [#7124](https://github.com/magento/magento2/pull/7124) -- Code documentation corrections (by @evktalo)
+ * [#7294](https://github.com/magento/magento2/pull/7294) -- Let less continue compilation if file is empty (by @timo-schmid)
+ * [#8648](https://github.com/magento/magento2/pull/8648) -- Remove the copyright year from file headers (by @jameshalsall)
+ * [#4897](https://github.com/magento/magento2/pull/4897) -- Schedule generation was broken (by @ajpevers)
+ * [#5503](https://github.com/magento/magento2/pull/5503) -- ACL titles are swapped (by @yireo)
+ * [#7344](https://github.com/magento/magento2/pull/7344) -- Fix checking active carrier against store (by @torreytsui)
+ * [#7221](https://github.com/magento/magento2/pull/7221) -- Typo fix (by @gastondisacco)
+ * [#8982](https://github.com/magento/magento2/pull/8982) -- Move blank theme dependencies out of Magento_Theme requirejs-config (by @mikeoloughlin)
+ * [#8930](https://github.com/magento/magento2/pull/8930) -- Improved check when CategoryProcessor attempts to create a new category (by @ccasciotti)
+ * [#8923](https://github.com/magento/magento2/pull/8923) -- Check of null result value in swatch-renderer.js (by @aholovan)
+ * [#9013](https://github.com/magento/magento2/pull/9013) -- Added JS Jasmine tests to Travis CI (by @Igloczek)
+ * [#9039](https://github.com/magento/magento2/pull/9039) -- Translation in adminhtml Import form (by @Nolwennig)
+ * [#9034](https://github.com/magento/magento2/pull/9034) -- Invalidate and refresh customer data sections on HTTP DELETE requests (by @Vinai)
+ * [#6322](https://github.com/magento/magento2/pull/6322) -- Admin product edit block getHeader is not used (by @kassner)
+ * [#7045](https://github.com/magento/magento2/pull/7045) -- In case something wrong with underlying products (by @Will-I4M)
+ * [#8568](https://github.com/magento/magento2/pull/8568) -- Fix quote's outdated shipping address overwriting PayPal Express shipping address (by @torreytsui)
+ * [#9057](https://github.com/magento/magento2/pull/9057) -- remove duplicate method call (#9017) (by @will-b)
+ * [#9080](https://github.com/magento/magento2/pull/9080) -- Fix grammar mistakes with subscriptions - fixes #7498 (by @sambolek)
+ * [#9076](https://github.com/magento/magento2/pull/9076) -- Fix typo (by @PieterCappelle)
+ * [#9061](https://github.com/magento/magento2/pull/9061) -- Empty resolvedScopeCodes when config cache is cleaned (by @andreas-wickberg-vaimo)
+ * [#9044](https://github.com/magento/magento2/pull/9044) -- Remove superfluous character in class (by @tkn98)
+ * [#9095](https://github.com/magento/magento2/pull/9095) -- Added translation to label argument xml. (by @mrkhoa99)
+ * [#9108](https://github.com/magento/magento2/pull/9108) -- Redundant expression */1 in crontab.xml (by @giacmir)
+ * [#9170](https://github.com/magento/magento2/pull/9170) -- Corrected class name in documentation. (by @dfelton)
+ * [#6778](https://github.com/magento/magento2/pull/6778) -- Changed locator class name for ObjectManager (by @dverkade)
+ * [#7556](https://github.com/magento/magento2/pull/7556) -- Fix merging nested in view.xml (by @torreytsui)
+ * [#8903](https://github.com/magento/magento2/pull/8903) -- Include Reply-To name in contact form email header (by @josephmcdermott)
+ * [#9140](https://github.com/magento/magento2/pull/9140) -- remove duplicate calls to initObjectManager in bootstrap class (by @sivajik34)
+ * [#9133](https://github.com/magento/magento2/pull/9133) -- Favicon folder added on gitignore (by @rafaelstz)
+ * [#9204](https://github.com/magento/magento2/pull/9204) -- FPT label not translatable in the totals on the cart page. (by @okorshenko)
+ * [#5043](https://github.com/magento/magento2/pull/5043) -- Change 'select' to 'query' in props (by @flancer64)
+ * [#5367](https://github.com/magento/magento2/pull/5367) -- Change pub/.htaccess MAGE_MODE comment (by @erikhansen)
+ * [#5742](https://github.com/magento/magento2/pull/5742) -- Collection walk method bug fix when specific callback function (by @jalogut)
+ * [#6385](https://github.com/magento/magento2/pull/6385) -- Replace EE License Placeholder text with filename (by @navarr)
+ * [#6443](https://github.com/magento/magento2/pull/6443) -- Refactor Option ResourceModel to allow price supporting types to be intercepted (by @navarr)
+ * [#6772](https://github.com/magento/magento2/pull/6772) -- Changed constructor to use an interface (by @dverkade)
+ * [#6910](https://github.com/magento/magento2/pull/6910) -- Good practice, license in readme file (by @rafaelstz)
+ * [#7506](https://github.com/magento/magento2/pull/7506) -- Add configurations for change email templates (by @kassner)
+ * [#7464](https://github.com/magento/magento2/pull/7464) -- Is Allowed Guest Checkout (by @hungvt)
+ * [#7900](https://github.com/magento/magento2/pull/7900) -- Setting proper resource name (by @ddattee)
+ * [#8462](https://github.com/magento/magento2/pull/8462) -- Fix product option files not copying to order dir. (by @evktalo)
+ * [#8824](https://github.com/magento/magento2/pull/8824) -- Popup-Modal not closing on Safari/Windows (by @Hansschouten)
+ * [#9062](https://github.com/magento/magento2/pull/9062) -- Upgrade JS dependencies (by @Igloczek)
+ * [#9084](https://github.com/magento/magento2/pull/9084) -- Fix Google Analytics typo in printing Account Number, fixes #7549 (by @sambolek)
+ * [#9112](https://github.com/magento/magento2/pull/9112) -- Fix attribute label on product page at different store views (by @tufahu)
+ * [#9103](https://github.com/magento/magento2/pull/9103) -- Cli info di (by @springerin)
+ * [#9165](https://github.com/magento/magento2/pull/9165) -- Improved text of exception message in case of error in module's composer.json (by @vovayatsyuk)
+ * [#9131](https://github.com/magento/magento2/pull/9131) -- Fix to allow Zend_Db_Expr as column default (by @scottsb)
+ * [#9221](https://github.com/magento/magento2/pull/9221) -- Avoid: Undefined index: value in app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php on line 157 in Ajax return (by @mhauri)
+ * [#9215](https://github.com/magento/magento2/pull/9215) -- Remove unused and invalid method (by @adragus-inviqa)
+ * [#9210](https://github.com/magento/magento2/pull/9210) -- Do not di:compile tests/ folder (by @kassner)
+ * [#5325](https://github.com/magento/magento2/pull/5325) -- update url guide to v2.0 (by @sIiiS)
+ * [#6452](https://github.com/magento/magento2/pull/6452) -- Fix Login Popup broken on iPad portrait (by @ihor-sviziev)
+ * [#9234](https://github.com/magento/magento2/pull/9234) -- Fix indentation in pub/.htaccess file (by @erikhansen)
+ * [#6266](https://github.com/magento/magento2/pull/6266) -- Change commit to rollBack on fail (by @flancer64)
+ * [#6792](https://github.com/magento/magento2/pull/6792) -- Make a hardcoded value in Customizable Options interceptable (by @navarr)
+ * [#9094](https://github.com/magento/magento2/pull/9094) -- Issue #2802, #1146: Fixing sitemap generation folder (by @JosephMaxwell)
+ * [#9124](https://github.com/magento/magento2/pull/9124) -- Solve issues with API (by @paales)
+ * [#9247](https://github.com/magento/magento2/pull/9247) -- Fixed layout handle for cms page (by @simpleadm)
+ * [#9257](https://github.com/magento/magento2/pull/9257) -- Fixed coding standard violations in the Framework\Message namespace (by @dverkade)
+ * [#9254](https://github.com/magento/magento2/pull/9254) -- Fixed coding standard violations in the Framework\Encryption namespace (by @dverkade)
+ * [#9253](https://github.com/magento/magento2/pull/9253) -- Fixed coding standard violations in the Framework\Event namespace (by @dverkade)
+ * [#9250](https://github.com/magento/magento2/pull/9250) -- Fixed coding standard violations in the Framework\Archive namespace (by @dverkade)
+ * [#9264](https://github.com/magento/magento2/pull/9264) -- Enable/Disable DB query logging commands (by @federivo)
+ * [#9260](https://github.com/magento/magento2/pull/9260) -- Fixed coding standard violations in Framework\HTTP namespace: (by @dverkade)
+ * [#9258](https://github.com/magento/magento2/pull/9258) -- Fixed coding standard violations in the Framework\Filesystem namespace (by @dverkade)
+ * [#9286](https://github.com/magento/magento2/pull/9286) -- [WIP] Varnish Vcl generator command (by @piotrkwiecinski)
+ * [#9282](https://github.com/magento/magento2/pull/9282) -- Fixed coding standard violations in the Framework\Filter namespace (by @dverkade)
+ * [#9281](https://github.com/magento/magento2/pull/9281) -- Fixed coding standard violations in Framework\Image namespace (by @dverkade)
+ * [#9298](https://github.com/magento/magento2/pull/9298) -- Remove php-5.6 environment from travis.yml (by @4quaternion)
+ * [#4816](https://github.com/magento/magento2/pull/4816) -- Fixed issue with grouped product name column renderer. It (by @wbyrnetx)
+ * [#5147](https://github.com/magento/magento2/pull/5147) -- Change value-assignment to a setValue call (by @wexo-team)
+ * [#5411](https://github.com/magento/magento2/pull/5411) -- #5236 fixes "The configuration parameter "componentType" is a required for "advanced_pricing_button" component" (by @Protazy21)
+ * [#7148](https://github.com/magento/magento2/pull/7148) -- Fix issue 7075 (by @rmsundar1)
+ * [#9027](https://github.com/magento/magento2/pull/9027) -- Make CSS minifying compatible with calc() CSS function (by @sambolek)
+ * [#9262](https://github.com/magento/magento2/pull/9262) -- Remove zend json from theme (by @dmanners)
+ * [#9261](https://github.com/magento/magento2/pull/9261) -- Remove zend json from weee (by @dmanners)
+ * [#9302](https://github.com/magento/magento2/pull/9302) -- Fixed coding standard violations in the Framework\Module namespace (by @dverkade)
+ * [#9299](https://github.com/magento/magento2/pull/9299) -- Improved check for attribute codes in "additional_attributes" field (by @ccasciotti)
+ * [#9293](https://github.com/magento/magento2/pull/9293) -- Deprecate unused navigation-menu.js file in blank theme (by @mikeoloughlin)
+ * [#9308](https://github.com/magento/magento2/pull/9308) -- Remove unnecessary FQCN in ObserverInterface (by @jameshalsall)
+ * [#9304](https://github.com/magento/magento2/pull/9304) -- Fixed coding standard violations in all Factory classes located in app/code (by @dverkade)
+ * [#9303](https://github.com/magento/magento2/pull/9303) -- Fixed coding standard violations in the Framework namespace (by @dverkade)
+ * [#9318](https://github.com/magento/magento2/pull/9318) -- Fixed coding standard violations in the Framework\Backup namespace (by @dverkade)
+ * [#9321](https://github.com/magento/magento2/pull/9321) -- Fixed coding standard violations in the Framework\Autoload, Framework\Session & Framework\Webapi namespaces (by @dverkade)
+ * [#9320](https://github.com/magento/magento2/pull/9320) -- Fixed coding standard violations in the Framework\Cache namespace (by @dverkade)
+ * [#9319](https://github.com/magento/magento2/pull/9319) -- Fixed coding standard violations in the Framework\Api namespace (by @dverkade)
+ * [#9334](https://github.com/magento/magento2/pull/9334) -- Fixed coding standard violations in the Framework\Controller, Framework\CSS, Framework\Phrase and Framework\Pricing namespace (by @dverkade)
+ * [#9330](https://github.com/magento/magento2/pull/9330) -- Fixed coding standard violations in the Framework\Data namespace (by @dverkade)
+ * [#9329](https://github.com/magento/magento2/pull/9329) -- Fixed coding standard violations in the Framework\Stdlib namespace (by @dverkade)
+ * [#9328](https://github.com/magento/magento2/pull/9328) -- Fixed coding standard violations in the Framework\Config namespace (by @dverkade)
+ * [#5372](https://github.com/magento/magento2/pull/5372) -- Fix filesystem permission issues (by @BlackIkeEagle)
+ * [#9129](https://github.com/magento/magento2/pull/9129) -- Product Wizard: Use result type Layout instead of page layout (by @klein0r)
+ * [#9352](https://github.com/magento/magento2/pull/9352) -- Fixed coding standard violations in the Framework\File namespace (by @dverkade)
+ * [#9351](https://github.com/magento/magento2/pull/9351) -- Fixed coding standard violations in the Framework\Locale namespace (by @dverkade)
+ * [#9350](https://github.com/magento/magento2/pull/9350) -- Fixed coding standard violations in the Framework\App namespace (by @dverkade)
+ * [#9355](https://github.com/magento/magento2/pull/9355) -- Fixed coding standard violations in the Framework\Test namespace (by @dverkade)
+ * [#9354](https://github.com/magento/magento2/pull/9354) -- Fixed coding standard violations in the Framework\Translate namespace (by @dverkade)
+ * [#9353](https://github.com/magento/magento2/pull/9353) -- Fixed coding standard violations in the Framework\DB namespace (by @dverkade)
+ * [#8955](https://github.com/magento/magento2/pull/8955) -- Remove context aggregation validation (see Issue #6114) (by @Vinai)
+ * [#9343](https://github.com/magento/magento2/pull/9343) -- Add logging to contact us form (by @JamesonNetworks)
+ * [#9414](https://github.com/magento/magento2/pull/9414) -- Use loadPlayer requirejs mapping (by @ntoombs19)
+ * [#9400](https://github.com/magento/magento2/pull/9400) -- Fix addIdFilter method (by @adrian-martinez-interactiv4)
+ * [#9363](https://github.com/magento/magento2/pull/9363) -- Add ability to inject exception code in LocalizedException (by @adragus-inviqa)
+ * [#9446](https://github.com/magento/magento2/pull/9446) -- Fix data deletion using the multiple delete command (by @Kenboy)
+ * [#9539](https://github.com/magento/magento2/pull/9539) -- fix for "Class Magento\Framework\Console\CLI not found" in case sensitive scenarios (by @EObukhovsky)
+ * [#9514](https://github.com/magento/magento2/pull/9514) -- Fix breadcrumbs extra space (by @VincentMarmiesse)
+ * [#8409](https://github.com/magento/magento2/pull/8409) -- Allow X-Forwarded-For to have multiple values (by @kassner)
+ * [#9093](https://github.com/magento/magento2/pull/9093) -- JS Static tests added to CI (ESLint + JSCS) (by @Igloczek)
+ * [#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)
+ * [#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)
+ * [#9617](https://github.com/magento/magento2/pull/9617) -- Exclude unnecessarily duplicated Travis CI build jobs (by @Igloczek)
+ * [#9622](https://github.com/magento/magento2/pull/9622) -- Zend_Wildfire deprecated, Firephp outdated, magento/magento2#9239 and magento/magento2#9241 (by @SolsWebdesign)
+ * [#9625](https://github.com/magento/magento2/pull/9625) -- Remove unused plugin (by @elzekool)
+ * [#9637](https://github.com/magento/magento2/pull/9637) -- Change "wan't" to "want" (by @Leland)
+ * [#7020](https://github.com/magento/magento2/pull/7020) -- Fixes #7006, sales_order_status_label does not support version control (by @ajpevers)
+ * [#7456](https://github.com/magento/magento2/pull/7456) -- Remove unused entity_id foreign key (by @mattjbarlow)
+ * [#7755](https://github.com/magento/magento2/pull/7755) -- Remove redundant check and return early (by @AydinHassan)
+ * [#9657](https://github.com/magento/magento2/pull/9657) -- Fixes Typo (by @riconeitzel)
+ * [#4903](https://github.com/magento/magento2/pull/4903) -- Fix undefined offset notice when no order states are set (by @adragus-inviqa)
+ * [#6344](https://github.com/magento/magento2/pull/6344) -- Zend instead of regex in getGetterReturnType (by @flancer64)
+ * [#9686](https://github.com/magento/magento2/pull/9686) -- Update scripts.js (by @redelschaap)
+ * [#9701](https://github.com/magento/magento2/pull/9701) -- Add missing payment info template for PDF generation (by @cmuench)
+ * [#9697](https://github.com/magento/magento2/pull/9697) -- Configure Travis CI to run functional tests (by @okolesnyk)
+ * [#9711](https://github.com/magento/magento2/pull/9711) -- Cookie Restriction Mode Overlay should not be cached by Varnish #6455 (by @bka)
+ * [#9713](https://github.com/magento/magento2/pull/9713) -- stringify cookie value to fix Google Analyitcs Tracking and Cookie Overlay #5596 (by @bka)
+ * [#6503](https://github.com/magento/magento2/pull/6503) -- Remove breadcrumbs for multistore homepage (by @PingusPepan)
+ * [#7330](https://github.com/magento/magento2/pull/7330) -- Fix Framework\Data\Collection::each() method (by @Vinai)
+ * [#8484](https://github.com/magento/magento2/pull/8484) -- Fix swatch-renderer.js product id and isProductViewExist (by @mimarcel)
+ * [#9348](https://github.com/magento/magento2/pull/9348) -- Replace framework's Zend_Session interface usage with SessionHandlerInterface (by @tdgroot)
+ * [#9654](https://github.com/magento/magento2/pull/9654) -- magento/magento2#7279 bill-to name and ship-to name truncated to 20 chars (by @SolsWebdesign)
+ * [#9627](https://github.com/magento/magento2/pull/9627) -- Fix coding standard in Magento AdminNotification module (by @dverkade)
+ * [#9714](https://github.com/magento/magento2/pull/9714) -- Can't delete last item in cart if Minimum Order is Enable #6151 (by @storbahn)
+ * [#9717](https://github.com/magento/magento2/pull/9717) -- use payment method name to make checkbox of agreements more unique #6207 (by @bka)
+ * [#9715](https://github.com/magento/magento2/pull/9715) -- #4272: v2.0.4 Credit memos with adjustment fees cannot be fully refunded with a second credit memo (by @mcspronko)
+ * [#9344](https://github.com/magento/magento2/pull/9344) -- Explace the direct usage of Zend_Json with a call to the Json Help class (by @dmanners)
+ * [#9475](https://github.com/magento/magento2/pull/9475) -- Update select.js (by @redelschaap)
+ * [#9600](https://github.com/magento/magento2/pull/9600) -- Do not hardcode product link types (by @kassner)
+ * [#9712](https://github.com/magento/magento2/pull/9712) -- Customer with unique attribute can't be saved #7844 (by @storbahn)
+ * [#9723](https://github.com/magento/magento2/pull/9723) -- Patch to allow multiple filter_url_params to function (by @southerncomputer)
+ * [#9721](https://github.com/magento/magento2/pull/9721) -- [BUGFIX][6244] Fix Issue with code label display in cart checkout. (by @diglin)
+ * [#9753](https://github.com/magento/magento2/pull/9753) -- Replace Zend_Json in the configurable product block test (by @dmanners)
+ * [#9777](https://github.com/magento/magento2/pull/9777) -- Fix for #5897: getIdentities relies on uninitialized collection (by @kassner)
+ * [#9772](https://github.com/magento/magento2/pull/9772) -- Allow for referenceBlock to include template argument (by @jissereitsma)
+ * [#9797](https://github.com/magento/magento2/pull/9797) -- Adding logo in media folder (by @rafaelstz)
+ * [#9409](https://github.com/magento/magento2/pull/9409) -- Add a name to the Composite\Fieldset\Options block directive (by @navarr)
+ * [#9665](https://github.com/magento/magento2/pull/9665) -- Fix for javascript "mixins" when 'urlArgs' is set in requirejs - issue 8221 (by @thelettuce)
+ * [#9835](https://github.com/magento/magento2/pull/9835) -- Fixes Mage.Cookies poor performance (by @wujashek)
+ * [#9430](https://github.com/magento/magento2/pull/9430) -- Fix wrong store id filter (by @mimarcel)
+ * [#9670](https://github.com/magento/magento2/pull/9670) -- Allow injection of Magento\Catalog\Model\View\Asset\ImageFactory (by @rolftimmermans)
+ * [#9778](https://github.com/magento/magento2/pull/9778) -- new CLI command: Enable Template Hints (by @miguelbalparda)
+ * [#9820](https://github.com/magento/magento2/pull/9820) -- [oauth] Fixes #9819 (by @EliasKotlyar)
+ * [#9859](https://github.com/magento/magento2/pull/9859) -- Removed unused $_customerSession property (by @edenreich)
+ * [#4450](https://github.com/magento/magento2/pull/4450) -- Add ability to use tree-massactions ("sub-menus") on Sales > Orders grid (by @ikk0)
+ * [#9368](https://github.com/magento/magento2/pull/9368) -- Redis sess: fix path for persistent_identifier & compression_threshold (by @LukeHandle)
+ * [#9690](https://github.com/magento/magento2/pull/9690) -- Add froogaloop library as a dependency to load-player module (by @ntoombs19)
+ * [#9813](https://github.com/magento/magento2/pull/9813) -- Use static:: to support late static bindings in Invoice and Creditmemo (by @jokeputs)
+ * [#9780](https://github.com/magento/magento2/pull/9780) -- Coupon codes not showing in invoice print out #9216 (by @naouibelgacem)
+ * [#9872](https://github.com/magento/magento2/pull/9872) -- Fixed issue causing static test failure to report success on Travis (by @davidalger)
+ * [#9890](https://github.com/magento/magento2/pull/9890) -- Improved error logging when trying to save a product (by @woutersamaey)
+ * [#9892](https://github.com/magento/magento2/pull/9892) -- Added .DS_Store to .gitignore for Mac users (by @woutersamaey)
+ * [#9873](https://github.com/magento/magento2/pull/9873) -- Fixes layered navigation options being cached using the wrong store id. (by @hostep)
+ * [#7405](https://github.com/magento/magento2/pull/7405) -- Update Curl.php (by @redelschaap)
+ * [#7780](https://github.com/magento/magento2/pull/7780) -- setup:di:compile returns exit code 0 if errors are found (by @pivulic)
+ * [#9157](https://github.com/magento/magento2/pull/9157) -- Return array of blocks as items instead of array of arrays (by @tkotosz)
+ * [#9810](https://github.com/magento/magento2/pull/9810) -- Fix bug linked product position not updated if product link already exists (by @jalogut)
+ * [#9824](https://github.com/magento/magento2/pull/9824) -- Email to a Friend feature (by @WaPoNe)
+ * [#9823](https://github.com/magento/magento2/pull/9823) -- Return array of pages as items instead of array of arrays (by @tkotosz)
+ * [#9922](https://github.com/magento/magento2/pull/9922) -- Fixes small backwards incompatibility issue created in MAGETWO-69728 (by @hostep)
+ * [#4891](https://github.com/magento/magento2/pull/4891) -- Remove faulty index subscription (by @ajpevers)
+ * [#7758](https://github.com/magento/magento2/pull/7758) -- Throw exception when attribute doesn't exitst (by @AydinHassan)
+ * [#8879](https://github.com/magento/magento2/pull/8879) -- add middle name to checkout address html templates #8878 (by @ajpevers)
+ * [#9251](https://github.com/magento/magento2/pull/9251) -- Fixed coding standard violations in the Framework\Validator namespace (by @dverkade)
+ * [#9525](https://github.com/magento/magento2/pull/9525) -- Fixed the Inconsistent Gift Options checkbox labels #9421 (by @vpiyappan)
+ * [#9905](https://github.com/magento/magento2/pull/9905) -- Fix composer validation (by @barbazul)
+ * [#9932](https://github.com/magento/magento2/pull/9932) -- Fix typo in comment (by @avoelkl)
+ * [#9306](https://github.com/magento/magento2/pull/9306) -- Fix PaymentTokenFactory interface to have the "Interface" at the end of the name. (by @dverkade)
+ * [#9391](https://github.com/magento/magento2/pull/9391) -- Fix depends per group in system.xml (by @osrecio)
+ * [#9902](https://github.com/magento/magento2/pull/9902) -- Fix static integrity classes tests in Windows (by @barbazul)
+ * [#9915](https://github.com/magento/magento2/pull/9915) -- suggestion from #9338: add some command/option to the deploy command to refresh the version (by @ajpevers)
+ * [#9925](https://github.com/magento/magento2/pull/9925) -- Fix #9924, prefill prefix and suffix in checkout shipping address (by @ajpevers)
+ * [#9941](https://github.com/magento/magento2/pull/9941) -- By default, show times in admin grids in the store timezone. (by @ajpevers)
+ * [#9943](https://github.com/magento/magento2/pull/9943) -- Cron uses the wrong timestamp method (by @ajpevers)
+ * [#9964](https://github.com/magento/magento2/pull/9964) -- Add target attribute to Magento_Ui grid (by @thelettuce)
+ * [#9973](https://github.com/magento/magento2/pull/9973) -- Fixed coding standard violations in the Magento\Wishlist namespace (by @dverkade)
+ * [#9974](https://github.com/magento/magento2/pull/9974) -- Fixed coding standard violations in the Magento\Backend namespace (by @dverkade)
+ * [#9975](https://github.com/magento/magento2/pull/9975) -- Fixed coding standard violations in the Magento\Cms namespace (by @dverkade)
+ * [#9978](https://github.com/magento/magento2/pull/9978) -- Fixed coding standard violations in the Magento\Authorization Magento\Backup Magento\Captcha Magento\CurrencySymbol and Magento\Dhl namespace (by @dverkade)
+ * [#8965](https://github.com/magento/magento2/pull/8965) -- Reduce calls to SplFileInfo::realpath() in the Magento\Setup\Module\Di\Code\Reader\ClassesScanner class (by @kschroeder)
+ * [#9996](https://github.com/magento/magento2/pull/9996) -- Ubuntu Trusty 14.04 images update (by @miguelbalparda)
+ * [#8784](https://github.com/magento/magento2/pull/8784) -- magento/magento2: #8616 (by @cavalier79)
+ * [#9939](https://github.com/magento/magento2/pull/9939) -- Retrieve taxes from the correct object (by @fooman)
+ * [#9957](https://github.com/magento/magento2/pull/9957) -- Instantly apply configuration changes in the cron schedule (by @ajpevers)
+ * [#9994](https://github.com/magento/magento2/pull/9994) -- Fix mini-cart not emptied for logged out users checking out with PayPal Express (by @driskell)
+ * [#9082](https://github.com/magento/magento2/pull/9082) -- Get sitemap product images from image cache, if available (by @sambolek)
+ * [#9786](https://github.com/magento/magento2/pull/9786) -- [#7291] Change the default contact form email template to HTML (by @VincentMarmiesse)
+ * [#9361](https://github.com/magento/magento2/pull/9361) -- Fixed coding standard violations in the Framework\Model namespace (by @dverkade)
+ * [#9359](https://github.com/magento/magento2/pull/9359) -- Fixed coding standard violations in the Framework\Interception namespace (by @dverkade)
+ * [#9358](https://github.com/magento/magento2/pull/9358) -- Fixed coding standard violations in the Framework\Code namespace (by @dverkade)
+ * [#9429](https://github.com/magento/magento2/pull/9429) -- Fix not detecting current store using store code in url using $storeResolver->getCurrentStoreId() (by @mimarcel)
+ * [#9362](https://github.com/magento/magento2/pull/9362) -- Fixed coding standard violations in the Framework\ObjectManager namespace (by @dverkade)
+ * [#9970](https://github.com/magento/magento2/pull/9970) -- Added public methods to make Sitemap model plugin friendly (by @7ochem)
+ * [#7729](https://github.com/magento/magento2/pull/7729) -- Allow USPS Shipping Methods Without ExtraServices (by @jaywilliams)
+ * [#9314](https://github.com/magento/magento2/pull/9314) -- Support null value for custom attributes. (by @meng-tian)
+ * [#10033](https://github.com/magento/magento2/pull/10033) -- Fix for file category image uploader (by @Bartlomiejsz)
+ * [#10047](https://github.com/magento/magento2/pull/10047) -- Include attribute code in error message (by @lazyguru)
+ * [#10056](https://github.com/magento/magento2/pull/10056) -- Translate password field placeholder in Checkout (by @mimarcel)
+ * [#7139](https://github.com/magento/magento2/pull/7139) -- Stickyjs improvements (by @vovayatsyuk)
+ * [#9681](https://github.com/magento/magento2/pull/9681) -- Issue 9680: Use parent name for variations (by @PascalBrouwers)
+ * [#10031](https://github.com/magento/magento2/pull/10031) -- Allow option disabling for optgroup binding (by @Bart-art)
+ * [#10060](https://github.com/magento/magento2/pull/10060) -- Adding escapeHtml to Newsletter phtml (by @rafaelstz)
+ * [#10062](https://github.com/magento/magento2/pull/10062) -- Fix formatting for USPS Carrier (by @ihor-sviziev)
+ * [#9672](https://github.com/magento/magento2/pull/9672) -- Revert minimum stability to stable, tasks #4359 (by @ktomk)
+ * [#9986](https://github.com/magento/magento2/pull/9986) -- Improved type hints and declarations for \Magento\Quote\Model\Quote\Address\Total (by @schmengler)
+ * [#10082](https://github.com/magento/magento2/pull/10082) -- M2 2266 (by @tzyganu)
+ * [#10086](https://github.com/magento/magento2/pull/10086) -- Fix condition for autoloader function definitions (by @miromichalicka)
+ * [#9611](https://github.com/magento/magento2/pull/9611) -- Admin Grid Mass action Select / Unselect All issue #9610 (by @minesh0111)
+ * [#9726](https://github.com/magento/magento2/pull/9726) -- Remove wrong '_setup' replace when getting DB connection (2) (by @jalogut)
+ * [#9754](https://github.com/magento/magento2/pull/9754) -- Remove zend json from form elements (by @dmanners)
+ * [#9992](https://github.com/magento/magento2/pull/9992) -- Make page title in layout files translatable (by @ajpevers)
+ * [#10052](https://github.com/magento/magento2/pull/10052) -- M2 5381 (by @tzyganu)
+ * [#1563](https://github.com/magento/magento2/pull/1563) -- Convert long form tags with echo to use short-echo tags (by @davidalger)
+ * [#4147](https://github.com/magento/magento2/pull/4147) -- Add filename paramenter log (by @fcapua-summa)
+ * [#10043](https://github.com/magento/magento2/pull/10043) -- Fix trailing slash used in url rewrites (by @ihor-sviziev)
+ * [#10106](https://github.com/magento/magento2/pull/10106) -- Return URL in getThumbnailUrl instead of nothing (by @samgranger)
+ * [#3889](https://github.com/magento/magento2/pull/3889) -- Add missing dependencies of magento/framework (by @GordonLesti)
+ * [#10114](https://github.com/magento/magento2/pull/10114) -- Add missing dependencies of magento/framework (by @okorshenko)
+ * [#7174](https://github.com/magento/magento2/pull/7174) -- Type hint for \DateTimeInterface instead of \DateTime (by @jameshalsall)
+ * [#9189](https://github.com/magento/magento2/pull/9189) -- Avoid duplicate ltrim function on not complied mode. (by @sivajik34)
+ * [#10094](https://github.com/magento/magento2/pull/10094) -- Fix for isoneof condition in catalogrule (by @duckchip)
+ * [#10105](https://github.com/magento/magento2/pull/10105) -- Add referrerPolicy to Vimeo Video iframe to allow domain-restricted videos (by @davefarthing)
+ * [#10126](https://github.com/magento/magento2/pull/10126) -- Fix width & height mapping during image upload (by @ihor-sviziev)
+ * [#10140](https://github.com/magento/magento2/pull/10140) -- Update attribute vat_id frontend_label to make it translatable (by @JeroenVanLeusden)
+ * [#10149](https://github.com/magento/magento2/pull/10149) -- Fix wrong ACL for Developer Section (by @PascalBrouwers)
+ * [#10151](https://github.com/magento/magento2/pull/10151) -- Fix Widget saving non-XML entities to layout_update (by @tdgroot)
+ * [#9588](https://github.com/magento/magento2/pull/9588) -- Support controller src_type for head links (by @kassner)
+ * [#9904](https://github.com/magento/magento2/pull/9904) -- Fixed pointless exception in logs every time a category with image is saved (by @woutersamaey)
+ * [#10059](https://github.com/magento/magento2/pull/10059) -- Fix fetching quote item by id (by @mladenilic)
+
2.1.0
=============
To get detailed information about changes in Magento 2.1.0, please visit [Magento Community Edition (CE) Release Notes](http://devdocs.magento.com/guides/v2.1/release-notes/ReleaseNotes2.1.0CE.html "Magento Community Edition (CE) Release Notes")
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/README.md b/README.md
index 9b1aa1b7b3e28..e90f6cfac8f8c 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,18 @@
-[![Build Status](https://travis-ci.org/magento/magento2.svg?branch=develop)](https://travis-ci.org/magento/magento2)
+[![Build Status](https://travis-ci.org/magento/magento2.svg?branch=2.2-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,8 +23,8 @@ 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]:
@@ -38,8 +39,10 @@ To suggest documentation improvements, click [here][4].
| ![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. |
+| ![in progress](http://devdocs.magento.com/common/images/github_inProgress.png) | The internal ticket is currently in progress, fix is scheduled to be delivered. |
+| ![needs update](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. |
+
+To learn more about issue gate labels click [here](https://github.com/magento/magento2/wiki/Magento-Issue-Gates)
Reporting security issues
diff --git a/app/.htaccess b/app/.htaccess
index 93169e4eb44ff..707c26b075e16 100644
--- a/app/.htaccess
+++ b/app/.htaccess
@@ -1,2 +1,8 @@
-Order deny,allow
-Deny from all
+
+ order allow,deny
+ deny from all
+
+= 2.4>
+ Require all denied
+
+
diff --git a/app/bootstrap.php b/app/bootstrap.php
index 6701a9f4dd51e..e77c6d432c816 100644
--- a/app/bootstrap.php
+++ b/app/bootstrap.php
@@ -14,12 +14,12 @@
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;
@@ -49,12 +49,17 @@
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
) {
+ $profilerFlag = isset($_SERVER['MAGE_PROFILER']) && strlen($_SERVER['MAGE_PROFILER'])
+ ? $_SERVER['MAGE_PROFILER']
+ : trim(file_get_contents(BP . '/var/profiler.flag'));
+
\Magento\Framework\Profiler::applyConfig(
- $_SERVER['MAGE_PROFILER'],
+ $profilerFlag,
BP,
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
);
diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php
index 6c0dfd1db7d16..94c7d955f592b 100644
--- a/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php
+++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/Notification/MassRemove.php
@@ -39,6 +39,6 @@ public function execute()
$this->messageManager->addException($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/composer.json b/app/code/Magento/AdminNotification/composer.json
index a3371c702c729..9f6951c477506 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",
"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.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
index 23829d3725119..0e8acb37104e6 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php
@@ -394,7 +394,7 @@ protected function saveAndReplaceAdvancedPrices()
? $rowData[self::COL_TIER_PRICE] : 0,
'percentage_value' => $rowData[self::COL_TIER_PRICE_TYPE] === self::TIER_PRICE_TYPE_PERCENT
? $rowData[self::COL_TIER_PRICE] : null,
- 'website_id' => $this->getWebsiteId($rowData[self::COL_TIER_PRICE_WEBSITE])
+ 'website_id' => $this->getWebSiteId($rowData[self::COL_TIER_PRICE_WEBSITE])
];
}
}
diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php
index 5111b4932d7a8..9a380ff75da24 100644
--- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php
+++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php
@@ -27,7 +27,7 @@ class WebsiteTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
- $this->webSiteModel = $this->getMockBuilder(\Magento\Store\Model\WebSite::class)
+ $this->webSiteModel = $this->getMockBuilder(\Magento\Store\Model\Website::class)
->setMethods(['getBaseCurrency'])
->disableOriginalConstructor()
->getMock();
diff --git a/app/code/Magento/AdvancedPricingImportExport/composer.json b/app/code/Magento/AdvancedPricingImportExport/composer.json
index ec54cfd1e5e62..4712cba1f6105 100644
--- a/app/code/Magento/AdvancedPricingImportExport/composer.json
+++ b/app/code/Magento/AdvancedPricingImportExport/composer.json
@@ -2,7 +2,7 @@
"name": "magento/module-advanced-pricing-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",
"magento/module-catalog": "102.0.*",
"magento/module-catalog-inventory": "100.2.*",
"magento/module-eav": "101.0.*",
@@ -13,7 +13,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.1",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Analytics/Api/Data/LinkInterface.php b/app/code/Magento/Analytics/Api/Data/LinkInterface.php
new file mode 100644
index 0000000000000..6597dff868b9f
--- /dev/null
+++ b/app/code/Magento/Analytics/Api/Data/LinkInterface.php
@@ -0,0 +1,24 @@
+' . $element->getLabel() . '';
+ $html .= '
';
+ return $this->decorateRowHtml($element, $html);
+ }
+
+ /**
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @param string $html
+ * @return string
+ */
+ private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html)
+ {
+ return sprintf(
+ ' ',
+ $element->getHtmlId(),
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php
new file mode 100644
index 0000000000000..34f2b7d53d9be
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/CollectionTimeLabel.php
@@ -0,0 +1,53 @@
+localeResolver = $localeResolver ?:
+ ObjectManager::getInstance()->get(\Magento\Framework\Locale\ResolverInterface::class);
+ parent::__construct($context, $data);
+ }
+
+ /**
+ * Add current time zone to comment, properly translated according to locale
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @return string
+ */
+ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element)
+ {
+ $timeZoneCode = $this->_localeDate->getConfigTimezone();
+ $locale = $this->localeResolver->getLocale();
+ $getLongTimeZoneName = \IntlTimeZone::createTimeZone($timeZoneCode)
+ ->getDisplayName(false, \IntlTimeZone::DISPLAY_LONG, $locale);
+ $element->setData(
+ 'comment',
+ sprintf("%s (%s)", $getLongTimeZoneName, $timeZoneCode)
+ );
+ return parent::render($element);
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php
new file mode 100644
index 0000000000000..c09213c7f009d
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/SubscriptionStatusLabel.php
@@ -0,0 +1,64 @@
+subscriptionStatusProvider = $labelStatusProvider;
+ }
+
+ /**
+ * Add Subscription status to comment
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @return string
+ */
+ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element)
+ {
+ $element->setData(
+ 'comment',
+ $this->prepareLabelValue()
+ );
+ return parent::render($element);
+ }
+
+ /**
+ * Prepare label for subscription status
+ *
+ * @return string
+ */
+ private function prepareLabelValue()
+ {
+ return __('Subscription status') . ': ' . __($this->subscriptionStatusProvider->getStatus());
+ }
+}
diff --git a/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php
new file mode 100644
index 0000000000000..99606e10f99d9
--- /dev/null
+++ b/app/code/Magento/Analytics/Block/Adminhtml/System/Config/Vertical.php
@@ -0,0 +1,41 @@
+' . $element->getHint() . '';
+ $html .= '';
+ return $this->decorateRowHtml($element, $html);
+ }
+
+ /**
+ * Decorates row HTML for custom element style
+ *
+ * @param \Magento\Framework\Data\Form\Element\AbstractElement $element
+ * @param string $html
+ * @return string
+ */
+ private function decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html)
+ {
+ $rowHtml = sprintf('%s ', $html);
+ $rowHtml .= sprintf(
+ '%s %s ',
+ $element->getHtmlId(),
+ $element->getLabelHtml($element->getHtmlId(), "[WEBSITE]"),
+ $element->getElementHtml()
+ );
+ return $rowHtml;
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php
new file mode 100644
index 0000000000000..a90a971cf41b4
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php
@@ -0,0 +1,64 @@
+config = $config;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::bi_essentials');
+ }
+
+ /**
+ * Provides link to BI Essentials signup
+ *
+ * @return \Magento\Framework\Controller\AbstractResult
+ */
+ public function execute()
+ {
+ return $this->resultRedirectFactory->create()->setUrl(
+ $this->config->getValue($this->urlBIEssentialsConfigPath)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php
new file mode 100644
index 0000000000000..1b0e5c92420de
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php
@@ -0,0 +1,75 @@
+reportUrlProvider = $reportUrlProvider;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller.
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Redirect to resource with reports.
+ *
+ * @return Redirect $resultRedirect
+ */
+ public function execute()
+ {
+ /** @var Redirect $resultRedirect */
+ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ try {
+ $resultRedirect->setUrl($this->reportUrlProvider->getUrl());
+ } catch (SubscriptionUpdateException $e) {
+ $this->getMessageManager()->addNoticeMessage($e->getMessage());
+ $resultRedirect->setPath('adminhtml');
+ } catch (LocalizedException $e) {
+ $this->getMessageManager()->addExceptionMessage($e, $e->getMessage());
+ $resultRedirect->setPath('adminhtml');
+ } catch (\Exception $e) {
+ $this->getMessageManager()->addExceptionMessage(
+ $e,
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ );
+ $resultRedirect->setPath('adminhtml');
+ }
+
+ return $resultRedirect;
+ }
+}
diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php
new file mode 100644
index 0000000000000..122cf74123cc9
--- /dev/null
+++ b/app/code/Magento/Analytics/Controller/Adminhtml/Subscription/Retry.php
@@ -0,0 +1,73 @@
+subscriptionHandler = $subscriptionHandler;
+ parent::__construct($context);
+ }
+
+ /**
+ * Check admin permissions for this controller
+ *
+ * @return boolean
+ */
+ protected function _isAllowed()
+ {
+ return $this->_authorization->isAllowed('Magento_Analytics::analytics_settings');
+ }
+
+ /**
+ * Retry process of subscription.
+ *
+ * @return Redirect
+ */
+ public function execute()
+ {
+ /** @var Redirect $resultRedirect */
+ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
+ try {
+ $resultRedirect->setPath('adminhtml');
+ $this->subscriptionHandler->processEnabled();
+ } catch (LocalizedException $e) {
+ $this->getMessageManager()->addExceptionMessage($e, $e->getMessage());
+ } catch (\Exception $e) {
+ $this->getMessageManager()->addExceptionMessage(
+ $e,
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ );
+ }
+
+ return $resultRedirect;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/CollectData.php b/app/code/Magento/Analytics/Cron/CollectData.php
new file mode 100644
index 0000000000000..ff0b3e4f67638
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/CollectData.php
@@ -0,0 +1,53 @@
+exportDataHandler = $exportDataHandler;
+ $this->subscriptionStatus = $subscriptionStatus;
+ }
+
+ /**
+ * @return bool
+ */
+ public function execute()
+ {
+ if ($this->subscriptionStatus->getStatus() === SubscriptionStatusProvider::ENABLED) {
+ $this->exportDataHandler->prepareExportData();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/SignUp.php b/app/code/Magento/Analytics/Cron/SignUp.php
new file mode 100644
index 0000000000000..c17b9b8c381c3
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/SignUp.php
@@ -0,0 +1,101 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->flagManager = $flagManager;
+ $this->reinitableConfig = $reinitableConfig;
+ }
+
+ /**
+ * Execute scheduled subscription operation
+ * In case of failure writes message to notifications inbox
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $attemptsCount = $this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+
+ if (($attemptsCount === null) || ($attemptsCount <= 0)) {
+ $this->deleteAnalyticsCronExpr();
+ $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ return false;
+ }
+
+ $attemptsCount -= 1;
+ $this->flagManager->saveFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount);
+ $signUpResult = $this->connector->execute('signUp');
+ if ($signUpResult === false) {
+ return false;
+ }
+
+ $this->deleteAnalyticsCronExpr();
+ $this->flagManager->deleteFlag(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ return true;
+ }
+
+ /**
+ * Delete cron schedule setting into config.
+ *
+ * Delete cron schedule setting for subscription handler into config and
+ * re-initialize config cache to avoid auto-generate new schedule items.
+ *
+ * @return bool
+ */
+ private function deleteAnalyticsCronExpr()
+ {
+ $this->configWriter->delete(SubscriptionHandler::CRON_STRING_PATH);
+ $this->reinitableConfig->reinit();
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Cron/Update.php b/app/code/Magento/Analytics/Cron/Update.php
new file mode 100644
index 0000000000000..9062a7bac7551
--- /dev/null
+++ b/app/code/Magento/Analytics/Cron/Update.php
@@ -0,0 +1,92 @@
+connector = $connector;
+ $this->configWriter = $configWriter;
+ $this->reinitableConfig = $reinitableConfig;
+ $this->flagManager = $flagManager;
+ $this->analyticsToken = $analyticsToken;
+ }
+
+ /**
+ * Execute scheduled update operation
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ $attemptsCount = $this->flagManager
+ ->getFlagData(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE);
+
+ if ($attemptsCount) {
+ $attemptsCount -= 1;
+ $result = $this->connector->execute('update');
+ }
+
+ if ($result || ($attemptsCount <= 0) || (!$this->analyticsToken->isTokenExist())) {
+ $this->flagManager
+ ->deleteFlag(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE);
+ $this->flagManager->deleteFlag(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE);
+ $this->configWriter->delete(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH);
+ $this->reinitableConfig->reinit();
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/LICENSE.txt b/app/code/Magento/Analytics/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/Analytics/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/Analytics/LICENSE_AFL.txt b/app/code/Magento/Analytics/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/Analytics/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/Analytics/Model/AnalyticsToken.php b/app/code/Magento/Analytics/Model/AnalyticsToken.php
new file mode 100644
index 0000000000000..ccec4d1bbe958
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/AnalyticsToken.php
@@ -0,0 +1,92 @@
+reinitableConfig = $reinitableConfig;
+ $this->config = $config;
+ $this->configWriter = $configWriter;
+ }
+
+ /**
+ * Get Magento BI token value.
+ *
+ * @return string|null
+ */
+ public function getToken()
+ {
+ return $this->config->getValue($this->tokenPath);
+ }
+
+ /**
+ * Stores Magento BI token value.
+ *
+ * @param string $value
+ *
+ * @return bool
+ */
+ public function storeToken($value)
+ {
+ $this->configWriter->save($this->tokenPath, $value);
+ $this->reinitableConfig->reinit();
+
+ return true;
+ }
+
+ /**
+ * Check Magento BI token value exist.
+ *
+ * @return bool
+ */
+ public function isTokenExist()
+ {
+ return (bool)$this->getToken();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config.php b/app/code/Magento/Analytics/Model/Config.php
new file mode 100644
index 0000000000000..ba508187b4b9f
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config.php
@@ -0,0 +1,40 @@
+data = $data;
+ }
+
+ /**
+ * Get config value by key.
+ *
+ * @param string|null $key
+ * @param string|null $default
+ * @return array
+ */
+ public function get($key = null, $default = null)
+ {
+ return $this->data->get($key, $default);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php
new file mode 100644
index 0000000000000..6e6f008d49f7e
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Baseurl/SubscriptionUpdateHandler.php
@@ -0,0 +1,107 @@
+analyticsToken = $analyticsToken;
+ $this->flagManager = $flagManager;
+ $this->reinitableConfig = $reinitableConfig;
+ $this->configWriter = $configWriter;
+ }
+
+ /**
+ * Activate process of subscription update handling.
+ *
+ * @param string $url
+ * @return bool
+ */
+ public function processUrlUpdate(string $url)
+ {
+ if ($this->analyticsToken->isTokenExist()) {
+ if (!$this->flagManager->getFlagData(self::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ $this->flagManager->saveFlag(self::PREVIOUS_BASE_URL_FLAG_CODE, $url);
+ }
+
+ $this->flagManager
+ ->saveFlag(self::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue);
+ $this->configWriter->save(self::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfig->reinit();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php
new file mode 100644
index 0000000000000..e26ad01fc74bf
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/CollectionTime.php
@@ -0,0 +1,91 @@
+configWriter = $configWriter;
+ parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * {@inheritdoc}. Set schedule setting for cron.
+ *
+ * @return Value
+ */
+ public function afterSave()
+ {
+ $result = preg_match('#(?\d{2}),(?\d{2}),(?\d{2})#', $this->getValue(), $time);
+
+ if (!$result) {
+ throw new LocalizedException(__('Time value has an unsupported format'));
+ }
+
+ $cronExprArray = [
+ $time['min'], # Minute
+ $time['hour'], # Hour
+ '*', # Day of the Month
+ '*', # Month of the Year
+ '*', # Day of the Week
+ ];
+
+ $cronExprString = join(' ', $cronExprArray);
+
+ try {
+ $this->configWriter->save(self::CRON_SCHEDULE_PATH, $cronExprString);
+ } catch (\Exception $e) {
+ $this->_logger->error($e->getMessage());
+ throw new LocalizedException(__('Cron settings can\'t be saved'));
+ }
+
+ return parent::afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php
new file mode 100644
index 0000000000000..ac97f2a843e61
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled.php
@@ -0,0 +1,84 @@
+subscriptionHandler = $subscriptionHandler;
+ parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
+ }
+
+ /**
+ * Add additional handling after config value was saved.
+ *
+ * @return Value
+ * @throws LocalizedException
+ */
+ public function afterSave()
+ {
+ try {
+ if ($this->isValueChanged()) {
+ $enabled = $this->getData('value');
+
+ if ($enabled) {
+ $this->subscriptionHandler->processEnabled();
+ } else {
+ $this->subscriptionHandler->processDisabled();
+ }
+ }
+ } catch (\Exception $e) {
+ $this->_logger->error($e->getMessage());
+ throw new LocalizedException(__('There was an error save new configuration value.'));
+ }
+
+ return parent::afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php
new file mode 100644
index 0000000000000..4b125949948c6
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Enabled/SubscriptionHandler.php
@@ -0,0 +1,172 @@
+configWriter = $configWriter;
+ $this->flagManager = $flagManager;
+ $this->analyticsToken = $analyticsToken;
+ $this->reinitableConfig = $reinitableConfig;
+ }
+
+ /**
+ * Processing of activation MBI subscription.
+ *
+ * Activate process of subscription handling if Analytics token is not received.
+ *
+ * @return bool
+ */
+ public function processEnabled()
+ {
+ if (!$this->analyticsToken->isTokenExist()) {
+ $this->setCronSchedule();
+ $this->setAttemptsFlag();
+ $this->reinitableConfig->reinit();
+ }
+
+ return true;
+ }
+
+ /**
+ * Set cron schedule setting into config for activation of subscription process.
+ *
+ * @return bool
+ */
+ private function setCronSchedule()
+ {
+ $this->configWriter->save(self::CRON_STRING_PATH, join(' ', self::CRON_EXPR_ARRAY));
+ return true;
+ }
+
+ /**
+ * Set flag as reserve counter of attempts subscription operation.
+ *
+ * @return bool
+ */
+ private function setAttemptsFlag()
+ {
+ return $this->flagManager
+ ->saveFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue);
+ }
+
+ /**
+ * Processing of deactivation MBI subscription.
+ *
+ * Disable data collection
+ * and interrupt subscription handling if Analytics token is not received.
+ *
+ * @return bool
+ */
+ public function processDisabled()
+ {
+ $this->disableCollectionData();
+
+ if (!$this->analyticsToken->isTokenExist()) {
+ $this->unsetAttemptsFlag();
+ }
+
+ return true;
+ }
+
+ /**
+ * Unset flag of attempts subscription operation.
+ *
+ * @return bool
+ */
+ private function unsetAttemptsFlag()
+ {
+ return $this->flagManager
+ ->deleteFlag(self::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ }
+
+ /**
+ * Unset schedule of collection data cron.
+ *
+ * @return bool
+ */
+ private function disableCollectionData()
+ {
+ $this->configWriter->delete(CollectionTime::CRON_SCHEDULE_PATH);
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php
new file mode 100644
index 0000000000000..1aabbb91ddf87
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Backend/Vertical.php
@@ -0,0 +1,32 @@
+getValue())) {
+ throw new LocalizedException(__('Please select a vertical.'));
+ }
+
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Mapper.php b/app/code/Magento/Analytics/Model/Config/Mapper.php
new file mode 100644
index 0000000000000..504690b8e4763
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Mapper.php
@@ -0,0 +1,66 @@
+ [
+ * 'name' => 'file_name',
+ * 'providers' => [
+ * 'reportProvider' => [
+ * 'name' => 'report_provider_name',
+ * 'class' => 'Magento\Analytics\ReportXml\ReportProvider',
+ * 'parameters' =>[
+ * 'name' => 'report_name',
+ * ],
+ * ],
+ * 'customProvider' => [
+ * 'name' => 'custom_provider_name',
+ * 'class' => 'Magento\Analytics\Model\CustomProvider',
+ * ],
+ * ],
+ * ]
+ * ];
+ */
+ public function execute($configData)
+ {
+ if (!isset($configData['config'][0]['file'])) {
+ return [];
+ }
+
+ $files = [];
+ foreach ($configData['config'][0]['file'] as $fileData) {
+ /** just one set of providers is allowed by xsd */
+ $providers = reset($fileData['providers']);
+ foreach ($providers as $providerType => $providerDataSet) {
+ /** just one set of provider data is allowed by xsd */
+ $providerData = reset($providerDataSet);
+ /** just one set of parameters is allowed by xsd */
+ $providerData['parameters'] = !empty($providerData['parameters'])
+ ? reset($providerData['parameters'])
+ : [];
+ $providerData['parameters'] = array_map(
+ 'reset',
+ $providerData['parameters']
+ );
+ $providers[$providerType] = $providerData;
+ }
+ $files[$fileData['name']] = $fileData;
+ $files[$fileData['name']]['providers'] = $providers;
+ }
+ return $files;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Reader.php b/app/code/Magento/Analytics/Model/Config/Reader.php
new file mode 100644
index 0000000000000..8980e31627717
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Reader.php
@@ -0,0 +1,52 @@
+mapper = $mapper;
+ $this->readers = $readers;
+ }
+
+ /**
+ * Read configuration scope.
+ *
+ * @param string|null $scope
+ * @return array
+ */
+ public function read($scope = null)
+ {
+ $data = [];
+ foreach ($this->readers as $reader) {
+ $data = array_merge_recursive($data, $reader->read($scope));
+ }
+
+ return $this->mapper->execute($data);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Config/Source/Vertical.php b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php
new file mode 100644
index 0000000000000..c9d9582ea7c7a
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Config/Source/Vertical.php
@@ -0,0 +1,51 @@
+verticals = $verticals;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toOptionArray()
+ {
+ $result = [
+ ['value' => '', 'label' => __('--Please Select--')]
+ ];
+
+ foreach ($this->verticals as $vertical) {
+ $result[] = ['value' => $vertical, 'label' => __($vertical)];
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ConfigInterface.php b/app/code/Magento/Analytics/Model/ConfigInterface.php
new file mode 100644
index 0000000000000..caaa2e100c1c7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ConfigInterface.php
@@ -0,0 +1,22 @@
+ 'command_class_name'.
+ *
+ * The list may be configured in each module via '/etc/di.xml'.
+ *
+ * @var string[]
+ */
+ private $commands;
+
+ /**
+ * @var ObjectManagerInterface
+ */
+ private $objectManager;
+
+ /**
+ * @param array $commands
+ * @param ObjectManagerInterface $objectManager
+ */
+ public function __construct(
+ array $commands,
+ ObjectManagerInterface $objectManager
+ ) {
+ $this->commands = $commands;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Executes a command in accordance with the given name.
+ *
+ * @param string $commandName
+ * @return bool
+ * @throws NotFoundException if the command is not found.
+ */
+ public function execute($commandName)
+ {
+ if (!array_key_exists($commandName, $this->commands)) {
+ throw new NotFoundException(__('Command was not found.'));
+ }
+
+ /** @var \Magento\Analytics\Model\Connector\CommandInterface $command */
+ $command = $this->objectManager->create($this->commands[$commandName]);
+
+ return $command->execute();
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/CommandInterface.php b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php
new file mode 100644
index 0000000000000..7a8774fe3dba9
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/CommandInterface.php
@@ -0,0 +1,21 @@
+curlFactory = $curlFactory;
+ $this->responseFactory = $responseFactory;
+ $this->converter = $converter;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function request($method, $url, array $body = [], array $headers = [], $version = '1.1')
+ {
+ $response = new \Zend_Http_Response(0, []);
+
+ try {
+ $curl = $this->curlFactory->create();
+ $headers = $this->applyContentTypeHeaderFromConverter($headers);
+
+ $curl->write($method, $url, $version, $headers, $this->converter->toBody($body));
+
+ $result = $curl->read();
+
+ if ($curl->getErrno()) {
+ $this->logger->critical(
+ new \Exception(
+ sprintf(
+ 'MBI service CURL connection error #%s: %s',
+ $curl->getErrno(),
+ $curl->getError()
+ )
+ )
+ );
+
+ return $response;
+ }
+
+ $response = $this->responseFactory->create($result);
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ }
+
+ return $response;
+ }
+
+ /**
+ * @param array $headers
+ *
+ * @return array
+ */
+ private function applyContentTypeHeaderFromConverter(array $headers)
+ {
+ $contentTypeHeaderKey = array_search($this->converter->getContentTypeHeader(), $headers);
+ if ($contentTypeHeaderKey === false) {
+ $headers[] = $this->converter->getContentTypeHeader();
+ }
+
+ return $headers;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php
new file mode 100644
index 0000000000000..a1e1f057684f6
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/Http/ClientInterface.php
@@ -0,0 +1,29 @@
+converter = $converter;
+ $this->responseHandlers = $responseHandlers;
+ }
+
+ /**
+ * @param \Zend_Http_Response $response
+ *
+ * @return bool|string
+ */
+ public function getResult(\Zend_Http_Response $response)
+ {
+ $result = false;
+ $responseBody = $this->converter->fromBody($response->getBody());
+ if (array_key_exists($response->getStatus(), $this->responseHandlers)) {
+ $result = $this->responseHandlers[$response->getStatus()]->handleResponse($responseBody);
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php
new file mode 100644
index 0000000000000..f1a8ea6460f9d
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/NotifyDataChangedCommand.php
@@ -0,0 +1,93 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->responseResolver = $responseResolver;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Notify MBI about that data collection was finished
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->notifyDataChangedUrlPath),
+ [
+ "access-token" => $this->analyticsToken->getToken(),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+ $result = $this->responseResolver->getResult($response);
+ }
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/OTPRequest.php b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php
new file mode 100644
index 0000000000000..dfa283e10d070
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/OTPRequest.php
@@ -0,0 +1,115 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->responseResolver = $responseResolver;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Performs obtaining of an OTP from the MBI service.
+ *
+ * Returns received OTP or FALSE in case of failure.
+ *
+ * @return string|false
+ */
+ public function call()
+ {
+ $result = false;
+
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->otpUrlConfigPath),
+ [
+ "access-token" => $this->analyticsToken->getToken(),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Obtaining of an OTP from the MBI service has been failed: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php
new file mode 100644
index 0000000000000..d9a672e81f43d
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/OTP.php
@@ -0,0 +1,24 @@
+analyticsToken = $analyticsToken;
+ $this->subscriptionHandler = $subscriptionHandler;
+ $this->subscriptionStatusProvider = $subscriptionStatusProvider;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function handleResponse(array $responseBody)
+ {
+ if ($this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::ENABLED) {
+ $this->analyticsToken->storeToken(null);
+ $this->subscriptionHandler->processEnabled();
+ }
+ return false;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php
new file mode 100644
index 0000000000000..b2261e418abc7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/SignUp.php
@@ -0,0 +1,51 @@
+analyticsToken = $analyticsToken;
+ $this->converter = $converter;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function handleResponse(array $body)
+ {
+ if (isset($body['access-token']) && !empty($body['access-token'])) {
+ $this->analyticsToken->storeToken($body['access-token']);
+ return $body['access-token'];
+ }
+
+ return false;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php
new file mode 100644
index 0000000000000..73fc575ae2821
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/ResponseHandler/Update.php
@@ -0,0 +1,24 @@
+analyticsToken = $analyticsToken;
+ $this->integrationManager = $integrationManager;
+ $this->config = $config;
+ $this->httpClient = $httpClient;
+ $this->logger = $logger;
+ $this->responseResolver = $responseResolver;
+ }
+
+ /**
+ * Executes signUp command
+ *
+ * During this call Magento generates or retrieves access token for the integration user
+ * In case successful generation Magento activates user and sends access token to MA
+ * As the response, Magento receives a token to MA
+ * Magento stores this token in System Configuration
+ *
+ * This method returns true in case of success
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ $integrationToken = $this->integrationManager->generateToken();
+ if ($integrationToken) {
+ $this->integrationManager->activateIntegration();
+ $response = $this->httpClient->request(
+ ZendClient::POST,
+ $this->config->getValue($this->signUpUrlPath),
+ [
+ "token" => $integrationToken->getData('token'),
+ "url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ ]
+ );
+
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Subscription for MBI service has been failed. An error occurred during token exchange: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php
new file mode 100644
index 0000000000000..8f05f1107e87e
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Connector/UpdateCommand.php
@@ -0,0 +1,114 @@
+analyticsToken = $analyticsToken;
+ $this->httpClient = $httpClient;
+ $this->config = $config;
+ $this->logger = $logger;
+ $this->flagManager = $flagManager;
+ $this->responseResolver = $responseResolver;
+ }
+
+ /**
+ * Executes update request to MBI api in case store url was changed
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $result = false;
+ if ($this->analyticsToken->isTokenExist()) {
+ $response = $this->httpClient->request(
+ ZendClient::PUT,
+ $this->config->getValue($this->updateUrlPath),
+ [
+ "url" => $this->flagManager
+ ->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE),
+ "new-url" => $this->config->getValue(Store::XML_PATH_SECURE_BASE_URL),
+ "access-token" => $this->analyticsToken->getToken(),
+ ]
+ );
+ $result = $this->responseResolver->getResult($response);
+ if (!$result) {
+ $this->logger->warning(
+ sprintf(
+ 'Update of the subscription for MBI service has been failed: %s',
+ !empty($response->getBody()) ? $response->getBody() : 'Response body is empty.'
+ )
+ );
+ }
+ }
+
+ return (bool)$result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Cryptographer.php b/app/code/Magento/Analytics/Model/Cryptographer.php
new file mode 100644
index 0000000000000..6905eee372ae2
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Cryptographer.php
@@ -0,0 +1,130 @@
+analyticsToken = $analyticsToken;
+ $this->encodedContextFactory = $encodedContextFactory;
+ }
+
+ /**
+ * Encrypt input data.
+ *
+ * @param string $source
+ * @return EncodedContext
+ * @throws LocalizedException
+ */
+ public function encode($source)
+ {
+ if (!is_string($source)) {
+ try {
+ $source = (string)$source;
+ } catch (\Exception $e) {
+ throw new LocalizedException(__('Input data must be string or convertible into string.'));
+ }
+ } elseif (!$source) {
+ throw new LocalizedException(__('Input data must be non-empty string.'));
+ }
+ if (!$this->validateCipherMethod($this->cipherMethod)) {
+ throw new LocalizedException(__('Not valid cipher method.'));
+ }
+ $initializationVector = $this->getInitializationVector();
+
+ $encodedContext = $this->encodedContextFactory->create([
+ 'content' => openssl_encrypt(
+ $source,
+ $this->cipherMethod,
+ $this->getKey(),
+ OPENSSL_RAW_DATA,
+ $initializationVector
+ ),
+ 'initializationVector' => $initializationVector,
+ ]);
+
+ return $encodedContext;
+ }
+
+ /**
+ * Return key for encryption.
+ *
+ * @return string
+ * @throws LocalizedException
+ */
+ private function getKey()
+ {
+ $token = $this->analyticsToken->getToken();
+ if (!$token) {
+ throw new LocalizedException(__('Encryption key can\'t be empty.'));
+ }
+ return hash('sha256', $token);
+ }
+
+ /**
+ * Return established cipher method.
+ *
+ * @return string
+ */
+ private function getCipherMethod()
+ {
+ return $this->cipherMethod;
+ }
+
+ /**
+ * Return each time generated random initialization vector which depends on the cipher method.
+ *
+ * @return string
+ */
+ private function getInitializationVector()
+ {
+ $ivSize = openssl_cipher_iv_length($this->getCipherMethod());
+ return openssl_random_pseudo_bytes($ivSize);
+ }
+
+ /**
+ * Check that cipher method is allowed for encryption.
+ *
+ * @param string $cipherMethod
+ * @return bool
+ */
+ private function validateCipherMethod($cipherMethod)
+ {
+ $methods = openssl_get_cipher_methods();
+ return (false !== array_search($cipherMethod, $methods));
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/EncodedContext.php b/app/code/Magento/Analytics/Model/EncodedContext.php
new file mode 100644
index 0000000000000..5fb2d0c15aef7
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/EncodedContext.php
@@ -0,0 +1,52 @@
+content = $content;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php
new file mode 100644
index 0000000000000..5d127037afea9
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Exception/State/SubscriptionUpdateException.php
@@ -0,0 +1,17 @@
+filesystem = $filesystem;
+ $this->archive = $archive;
+ $this->reportWriter = $reportWriter;
+ $this->cryptographer = $cryptographer;
+ $this->fileRecorder = $fileRecorder;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function prepareExportData()
+ {
+ try {
+ $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP);
+
+ $this->prepareDirectory($tmpDirectory, $this->getTmpFilesDirRelativePath());
+ $this->reportWriter->write($tmpDirectory, $this->getTmpFilesDirRelativePath());
+
+ $tmpFilesDirectoryAbsolutePath = $this->validateSource($tmpDirectory, $this->getTmpFilesDirRelativePath());
+ $archiveAbsolutePath = $this->prepareFileDirectory($tmpDirectory, $this->getArchiveRelativePath());
+ $this->pack(
+ $tmpFilesDirectoryAbsolutePath,
+ $archiveAbsolutePath
+ );
+
+ $this->validateSource($tmpDirectory, $this->getArchiveRelativePath());
+ $this->fileRecorder->recordNewFile(
+ $this->cryptographer->encode($tmpDirectory->readFile($this->getArchiveRelativePath()))
+ );
+ } finally {
+ $tmpDirectory->delete($this->getTmpFilesDirRelativePath());
+ $tmpDirectory->delete($this->getArchiveRelativePath());
+ }
+
+ return true;
+ }
+
+ /**
+ * Return relative path to a directory for temporary files with reports data.
+ *
+ * @return string
+ */
+ private function getTmpFilesDirRelativePath()
+ {
+ return $this->subdirectoryPath . 'tmp/';
+ }
+
+ /**
+ * Return relative path to a directory for an archive.
+ *
+ * @return string
+ */
+ private function getArchiveRelativePath()
+ {
+ return $this->subdirectoryPath . $this->archiveName;
+ }
+
+ /**
+ * Clean up a directory.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ */
+ private function prepareDirectory(WriteInterface $directory, $path)
+ {
+ $directory->delete($path);
+
+ return $directory->getAbsolutePath($path);
+ }
+
+ /**
+ * Remove a file and a create parent directory a file.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ */
+ private function prepareFileDirectory(WriteInterface $directory, $path)
+ {
+ $directory->delete($path);
+ if (dirname($path) !== '.') {
+ $directory->create(dirname($path));
+ }
+
+ return $directory->getAbsolutePath($path);
+ }
+
+ /**
+ * Packing data into an archive.
+ *
+ * @param string $source
+ * @param string $destination
+ * @return bool
+ */
+ private function pack($source, $destination)
+ {
+ $this->archive->pack(
+ $source,
+ $destination,
+ is_dir($source) ?: false
+ );
+
+ return true;
+ }
+
+ /**
+ * Validate that data source exist.
+ *
+ * Return absolute path in a validated data source.
+ *
+ * @param WriteInterface $directory
+ * @param string $path
+ * @return string
+ * @throws LocalizedException If source is not exist.
+ */
+ private function validateSource(WriteInterface $directory, $path)
+ {
+ if (!$directory->isExist($path)) {
+ throw new LocalizedException(__('Source "%1" is not exist', $directory->getAbsolutePath($path)));
+ }
+
+ return $directory->getAbsolutePath($path);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php
new file mode 100644
index 0000000000000..65efb33659c89
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ExportDataHandlerInterface.php
@@ -0,0 +1,19 @@
+exportDataHandler = $exportDataHandler;
+ $this->analyticsConnector = $connector;
+ }
+
+ /**
+ * {@inheritdoc}
+ * Execute notification command.
+ *
+ * @return bool
+ */
+ public function prepareExportData()
+ {
+ $result = $this->exportDataHandler->prepareExportData();
+ $this->analyticsConnector->execute('notifyDataChanged');
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileInfo.php b/app/code/Magento/Analytics/Model/FileInfo.php
new file mode 100644
index 0000000000000..19bdaf21b2a20
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileInfo.php
@@ -0,0 +1,52 @@
+path = $path;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileInfoManager.php b/app/code/Magento/Analytics/Model/FileInfoManager.php
new file mode 100644
index 0000000000000..e37700e665420
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileInfoManager.php
@@ -0,0 +1,123 @@
+flagManager = $flagManager;
+ $this->fileInfoFactory = $fileInfoFactory;
+ }
+
+ /**
+ * Save FileInfo object.
+ *
+ * @param FileInfo $fileInfo
+ * @return bool
+ * @throws LocalizedException
+ */
+ public function save(FileInfo $fileInfo)
+ {
+ $parameters = [];
+ $parameters['initializationVector'] = $fileInfo->getInitializationVector();
+ $parameters['path'] = $fileInfo->getPath();
+
+ $emptyParameters = array_diff($parameters, array_filter($parameters));
+ if ($emptyParameters) {
+ throw new LocalizedException(
+ __('These arguments can\'t be empty "%1"', implode(', ', array_keys($emptyParameters)))
+ );
+ }
+
+ foreach ($this->encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = $this->encodeValue($parameters[$encodedParameter]);
+ }
+
+ $this->flagManager->saveFlag($this->flagCode, $parameters);
+
+ return true;
+ }
+
+ /**
+ * Load FileInfo object.
+ *
+ * @return FileInfo
+ */
+ public function load()
+ {
+ $parameters = $this->flagManager->getFlagData($this->flagCode) ?: [];
+
+ $encodedParameters = array_intersect($this->encodedParameters, array_keys($parameters));
+ foreach ($encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = $this->decodeValue($parameters[$encodedParameter]);
+ }
+
+ $fileInfo = $this->fileInfoFactory->create($parameters);
+
+ return $fileInfo;
+ }
+
+ /**
+ * Encode value.
+ *
+ * @param string $value
+ * @return string
+ */
+ private function encodeValue($value)
+ {
+ return base64_encode($value);
+ }
+
+ /**
+ * Decode value.
+ *
+ * @param string $value
+ * @return string
+ */
+ private function decodeValue($value)
+ {
+ return base64_decode($value);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/FileRecorder.php b/app/code/Magento/Analytics/Model/FileRecorder.php
new file mode 100644
index 0000000000000..70438a98d56f1
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/FileRecorder.php
@@ -0,0 +1,136 @@
+fileInfoManager = $fileInfoManager;
+ $this->fileInfoFactory = $fileInfoFactory;
+ $this->filesystem = $filesystem;
+ }
+
+ /**
+ * Save new encrypted file, register it and remove old registered file.
+ *
+ * @param EncodedContext $encodedContext
+ * @return bool
+ */
+ public function recordNewFile(EncodedContext $encodedContext)
+ {
+ $directory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
+
+ $fileRelativePath = $this->getFileRelativePath();
+ $directory->writeFile($fileRelativePath, $encodedContext->getContent());
+
+ $fileInfo = $this->fileInfoManager->load();
+ $this->registerFile($encodedContext, $fileRelativePath);
+ $this->removeOldFile($fileInfo, $directory);
+
+ return true;
+ }
+
+ /**
+ * Return relative path to encoded file.
+ *
+ * @return string
+ */
+ private function getFileRelativePath()
+ {
+ return $this->fileSubdirectoryPath . hash('sha256', time())
+ . '/' . $this->encodedFileName;
+ }
+
+ /**
+ * Register encoded file.
+ *
+ * @param EncodedContext $encodedContext
+ * @param string $fileRelativePath
+ * @return bool
+ */
+ private function registerFile(EncodedContext $encodedContext, $fileRelativePath)
+ {
+ $newFileInfo = $this->fileInfoFactory->create(
+ [
+ 'path' => $fileRelativePath,
+ 'initializationVector' => $encodedContext->getInitializationVector(),
+ ]
+ );
+ $this->fileInfoManager->save($newFileInfo);
+
+ return true;
+ }
+
+ /**
+ * Remove previously registered file.
+ *
+ * @param FileInfo $fileInfo
+ * @param WriteInterface $directory
+ * @return bool
+ */
+ private function removeOldFile(FileInfo $fileInfo, WriteInterface $directory)
+ {
+ if (!$fileInfo->getPath()) {
+ return true;
+ }
+
+ $directory->delete($fileInfo->getPath());
+
+ $directoryName = dirname($fileInfo->getPath());
+ if ($directoryName !== '.') {
+ $directory->delete($directoryName);
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/IntegrationManager.php b/app/code/Magento/Analytics/Model/IntegrationManager.php
new file mode 100644
index 0000000000000..61a40a955e026
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/IntegrationManager.php
@@ -0,0 +1,126 @@
+integrationService = $integrationService;
+ $this->config = $config;
+ $this->oauthService = $oauthService;
+ }
+
+ /**
+ * Activate predefined integration user
+ *
+ * @return bool
+ * @throws NoSuchEntityException
+ */
+ public function activateIntegration()
+ {
+ $integration = $this->integrationService->findByName(
+ $this->config->getConfigDataValue('analytics/integration_name')
+ );
+ if (!$integration->getId()) {
+ throw new NoSuchEntityException(__('Cannot find predefined integration user!'));
+ }
+ $integrationData = $this->getIntegrationData(Integration::STATUS_ACTIVE);
+ $integrationData['integration_id'] = $integration->getId();
+ $this->integrationService->update($integrationData);
+ return true;
+ }
+
+ /**
+ * This method execute Generate Token command and enable integration
+ *
+ * @return bool|\Magento\Integration\Model\Oauth\Token
+ */
+ public function generateToken()
+ {
+ $consumerId = $this->generateIntegration()->getConsumerId();
+ $accessToken = $this->oauthService->getAccessToken($consumerId);
+ if (!$accessToken && $this->oauthService->createAccessToken($consumerId, true)) {
+ $accessToken = $this->oauthService->getAccessToken($consumerId);
+ }
+ return $accessToken;
+ }
+
+ /**
+ * Returns consumer Id for MA integration user
+ *
+ * @return \Magento\Integration\Model\Integration
+ */
+ private function generateIntegration()
+ {
+ $integration = $this->integrationService->findByName(
+ $this->config->getConfigDataValue('analytics/integration_name')
+ );
+ if (!$integration->getId()) {
+ $integration = $this->integrationService->create($this->getIntegrationData());
+ }
+ return $integration;
+ }
+
+ /**
+ * Returns default attributes for MA integration user
+ *
+ * @param int $status
+ * @return array
+ */
+ private function getIntegrationData($status = Integration::STATUS_INACTIVE)
+ {
+ $integrationData = [
+ 'name' => $this->config->getConfigDataValue('analytics/integration_name'),
+ 'status' => $status,
+ 'all_resources' => false,
+ 'resource' => [
+ 'Magento_Analytics::analytics',
+ 'Magento_Analytics::analytics_api'
+ ],
+ ];
+ return $integrationData;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Link.php b/app/code/Magento/Analytics/Model/Link.php
new file mode 100644
index 0000000000000..4a40796df4fd0
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Link.php
@@ -0,0 +1,54 @@
+url = $url;
+ $this->initializationVector = $initializationVector;
+ }
+
+ /**
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getInitializationVector()
+ {
+ return $this->initializationVector;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/LinkProvider.php b/app/code/Magento/Analytics/Model/LinkProvider.php
new file mode 100644
index 0000000000000..2474653f4916c
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/LinkProvider.php
@@ -0,0 +1,87 @@
+linkFactory = $linkFactory;
+ $this->fileInfoManager = $fileInfoManager;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Returns base url to file according to store configuration
+ *
+ * @param FileInfo $fileInfo
+ * @return string
+ */
+ private function getBaseUrl(FileInfo $fileInfo)
+ {
+ return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $fileInfo->getPath();
+ }
+
+ /**
+ * Verify is requested file ready
+ *
+ * @param FileInfo $fileInfo
+ * @return bool
+ */
+ private function isFileReady(FileInfo $fileInfo)
+ {
+ return $fileInfo->getPath() && $fileInfo->getInitializationVector();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get()
+ {
+ $fileInfo = $this->fileInfoManager->load();
+ if (!$this->isFileReady($fileInfo)) {
+ throw new NoSuchEntityException(__('File is not ready yet.'));
+ }
+ return $this->linkFactory->create(
+ [
+ 'url' => $this->getBaseUrl($fileInfo),
+ 'initializationVector' => base64_encode($fileInfo->getInitializationVector())
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php
new file mode 100644
index 0000000000000..174272614fb19
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/Plugin/BaseUrlConfigPlugin.php
@@ -0,0 +1,61 @@
+subscriptionUpdateHandler = $subscriptionUpdateHandler;
+ }
+
+ /**
+ * Add additional handling after config value was saved.
+ *
+ * @param Value $subject
+ * @param Value $result
+ * @return Value
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterAfterSave(
+ Value $subject,
+ Value $result
+ ) {
+ if ($this->isPluginApplicable($result)) {
+ $this->subscriptionUpdateHandler->processUrlUpdate($result->getOldValue());
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param Value $result
+ * @return bool
+ */
+ private function isPluginApplicable(Value $result)
+ {
+ return $result->isValueChanged()
+ && ($result->getPath() === Store::XML_PATH_SECURE_BASE_URL)
+ && ($result->getScope() === ScopeConfigInterface::SCOPE_TYPE_DEFAULT);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ProviderFactory.php b/app/code/Magento/Analytics/Model/ProviderFactory.php
new file mode 100644
index 0000000000000..3a23430fca077
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ProviderFactory.php
@@ -0,0 +1,39 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * @param string $providerName
+ * @return object
+ */
+ public function create($providerName)
+ {
+ return $this->objectManager->get($providerName);
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportUrlProvider.php b/app/code/Magento/Analytics/Model/ReportUrlProvider.php
new file mode 100644
index 0000000000000..e7fdf6f9e8132
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportUrlProvider.php
@@ -0,0 +1,94 @@
+analyticsToken = $analyticsToken;
+ $this->otpRequest = $otpRequest;
+ $this->config = $config;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Provide URL on resource with reports.
+ *
+ * @return string
+ * @throws SubscriptionUpdateException
+ */
+ public function getUrl()
+ {
+ if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ throw new SubscriptionUpdateException(__(
+ 'Your Base URL has been changed and your reports are being updated. '
+ . 'Advanced Reporting will be available once this change has been processed. Please try again later.'
+ ));
+ }
+
+ $url = $this->config->getValue($this->urlReportConfigPath);
+ if ($this->analyticsToken->isTokenExist()) {
+ $otp = $this->otpRequest->call();
+ if ($otp) {
+ $query = http_build_query(['otp' => $otp], '', '&');
+ $url .= '?' . $query;
+ }
+ }
+
+ return $url;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportWriter.php b/app/code/Magento/Analytics/Model/ReportWriter.php
new file mode 100644
index 0000000000000..7128658947908
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportWriter.php
@@ -0,0 +1,101 @@
+config = $config;
+ $this->reportValidator = $reportValidator;
+ $this->providerFactory = $providerFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write(WriteInterface $directory, $path)
+ {
+ $errorsList = [];
+ foreach ($this->config->get() as $file) {
+ $provider = reset($file['providers']);
+ if (isset($provider['parameters']['name'])) {
+ $error = $this->reportValidator->validate($provider['parameters']['name']);
+ if ($error) {
+ $errorsList[] = $error;
+ continue;
+ }
+ }
+ /** @var $providerObject */
+ $providerObject = $this->providerFactory->create($provider['class']);
+ $fileName = $provider['parameters'] ? $provider['parameters']['name'] : $provider['name'];
+ $fileFullPath = $path . $fileName . '.csv';
+ $fileData = $providerObject->getReport(...array_values($provider['parameters']));
+ $stream = $directory->openFile($fileFullPath, 'w+');
+ $stream->lock();
+ $headers = [];
+ foreach ($fileData as $row) {
+ if (!$headers) {
+ $headers = array_keys($row);
+ $stream->writeCsv($headers);
+ }
+ $stream->writeCsv($row);
+ }
+ $stream->unlock();
+ $stream->close();
+ }
+ if ($errorsList) {
+ $errorStream = $directory->openFile($path . $this->errorsFileName, 'w+');
+ foreach ($errorsList as $error) {
+ $errorStream->lock();
+ $errorStream->writeCsv($error);
+ $errorStream->unlock();
+ }
+ $errorStream->close();
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/ReportWriterInterface.php b/app/code/Magento/Analytics/Model/ReportWriterInterface.php
new file mode 100644
index 0000000000000..a611095a47ae4
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/ReportWriterInterface.php
@@ -0,0 +1,28 @@
+moduleManager = $moduleManager;
+ }
+
+ /**
+ * Returns module with module status
+ *
+ * @return array
+ */
+ public function current()
+ {
+ $current = parent::current();
+ if (is_array($current) && isset($current['module_name'])) {
+ $current['status'] =
+ $this->moduleManager->isEnabled($current['module_name']) == 1 ? 'Enabled' : "Disabled";
+ }
+ return $current;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php
new file mode 100644
index 0000000000000..0d226a9de7dc2
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/StoreConfigurationProvider.php
@@ -0,0 +1,102 @@
+scopeConfig = $scopeConfig;
+ $this->configPaths = $configPaths;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * Generates report using config paths from di.xml
+ * For each website and store
+ * @return \IteratorIterator
+ */
+ public function getReport()
+ {
+ $configReport = $this->generateReportForScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 0);
+
+ /** @var WebsiteInterface $website */
+ foreach ($this->storeManager->getWebsites() as $website) {
+ $configReport = array_merge(
+ $this->generateReportForScope(ScopeInterface::SCOPE_WEBSITES, $website->getId()),
+ $configReport
+ );
+ }
+
+ /** @var StoreInterface $store */
+ foreach ($this->storeManager->getStores() as $store) {
+ $configReport = array_merge(
+ $this->generateReportForScope(ScopeInterface::SCOPE_STORES, $store->getId()),
+ $configReport
+ );
+ }
+ return new \IteratorIterator(new \ArrayIterator($configReport));
+ }
+
+ /**
+ * Creates report from config for scope type and scope id.
+ *
+ * @param string $scope
+ * @param int $scopeId
+ * @return array
+ */
+ private function generateReportForScope($scope, $scopeId)
+ {
+ $report = [];
+ foreach ($this->configPaths as $configPath) {
+ $report[] = [
+ "config_path" => $configPath,
+ "scope" => $scope,
+ "scope_id" => $scopeId,
+ "value" => $this->scopeConfig->getValue(
+ $configPath,
+ $scope,
+ $scopeId
+ )
+ ];
+ }
+ return $report;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
new file mode 100644
index 0000000000000..1dd831a672faa
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/SubscriptionStatusProvider.php
@@ -0,0 +1,120 @@
+scopeConfig = $scopeConfig;
+ $this->analyticsToken = $analyticsToken;
+ $this->flagManager = $flagManager;
+ }
+
+ /**
+ * Retrieve subscription status to Magento BI Advanced Reporting.
+ *
+ * Statuses:
+ * Enabled - if subscription is enabled and MA token was received;
+ * Pending - if subscription is enabled and MA token was not received;
+ * Disabled - if subscription is not enabled.
+ * Failed - if subscription is enabled and token was not received after attempts ended.
+ *
+ * @return string
+ */
+ public function getStatus()
+ {
+ $isSubscriptionEnabledInConfig = $this->scopeConfig->getValue('analytics/subscription/enabled');
+ if ($isSubscriptionEnabledInConfig) {
+ return $this->getStatusForEnabledSubscription();
+ }
+
+ return $this->getStatusForDisabledSubscription();
+ }
+
+ /**
+ * Retrieve status for subscription that enabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForEnabledSubscription()
+ {
+ $status = static::ENABLED;
+ if ($this->flagManager->getFlagData(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)) {
+ $status = self::PENDING;
+ }
+
+ if (!$this->analyticsToken->isTokenExist()) {
+ $status = static::PENDING;
+ if ($this->flagManager->getFlagData(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE) === null) {
+ $status = static::FAILED;
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * Retrieve status for subscription that disabled in config.
+ *
+ * @return string
+ */
+ public function getStatusForDisabledSubscription()
+ {
+ return static::DISABLED;
+ }
+}
diff --git a/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php
new file mode 100644
index 0000000000000..9aaa2ebb3b56f
--- /dev/null
+++ b/app/code/Magento/Analytics/Model/System/Message/NotificationAboutFailedSubscription.php
@@ -0,0 +1,80 @@
+subscriptionStatusProvider = $subscriptionStatusProvider;
+ $this->urlBuilder = $urlBuilder;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @codeCoverageIgnore
+ */
+ public function getIdentity()
+ {
+ return hash('sha256', 'ANALYTICS_NOTIFICATION');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDisplayed()
+ {
+ return $this->subscriptionStatusProvider->getStatus() === SubscriptionStatusProvider::FAILED;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getText()
+ {
+ $messageDetails = '';
+
+ $messageDetails .= __('Failed to synchronize data to the Magento Business Intelligence service. ');
+ $messageDetails .= __(
+ 'Retry Synchronization ',
+ $this->urlBuilder->getUrl('analytics/subscription/retry')
+ );
+
+ return $messageDetails;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @codeCoverageIgnore
+ */
+ public function getSeverity()
+ {
+ return self::SEVERITY_MAJOR;
+ }
+}
diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md
new file mode 100644
index 0000000000000..7ec64abcd9b86
--- /dev/null
+++ b/app/code/Magento/Analytics/README.md
@@ -0,0 +1,41 @@
+# Magento_Analytics Module
+
+The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html) functionality.
+
+The module implements the following functionality:
+
+* enabling subscription to the MBI and automatic re-subscription
+* changing the base URL with the same MBI account remained
+* declaring the configuration schemas for report data collection
+* collecting the Magento instance data as reports for the MBI
+* introducing API that provides the collected data
+* extending Magento configuration with the module parameters:
+ * subscription status (enabled/disabled)
+ * industry (a business area in which the instance website works)
+ * time of data collection (time of the day when the module collects data)
+
+## Structure
+
+Beyond the [usual module file structure](http://devdocs.magento.com/guides/v2.2/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`.
+[Report XML](http://devdocs.magento.com/guides/v2.2/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting.
+The language declares SQL queries using XML declaration.
+
+## Subscription Process
+
+The subscription to the MBI service is enabled during the installation process of the Analytics module. Each administrator will be notified of these new features upon their initial login to the Admin Panel.
+
+## Analytics Settings
+
+Configuration settings for the Analytics module can be modified in the Admin Panel on the Stores > Configuration page under the General > Advanced Reporting tab.
+
+The following options can be adjusted:
+* Advanced Reporting Service (Enabled/Disabled)
+ * Alters the status of the Advanced Reporting subscription
+* Time of day to send data (Hour/Minute/Second in the store's time zone)
+ * Defines when the data collection process for the Advanced Reporting service occurs
+* Industry
+ * Defines the industry of the store in order to create a personalized Advanced Reporting experience
+
+## Extensibility
+
+We do not recommend to extend the Magento_Analytics module. It introduces an API that is purposed to transfer the collected data. Note that the API cannot be used for other needs.
diff --git a/app/code/Magento/Analytics/ReportXml/Config.php b/app/code/Magento/Analytics/ReportXml/Config.php
new file mode 100644
index 0000000000000..f50dcf941bf50
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config.php
@@ -0,0 +1,43 @@
+data = $data;
+ }
+
+ /**
+ * Returns config value by name
+ *
+ * @param string $queryName
+ * @return array
+ */
+ public function get($queryName)
+ {
+ return $this->data->get($queryName);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php
new file mode 100644
index 0000000000000..9e0b20a6ad414
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Converter/Xml.php
@@ -0,0 +1,61 @@
+hasAttributes()) {
+ $attrs = $source->attributes;
+ foreach ($attrs as $attr) {
+ $result[$attr->name] = $attr->value;
+ }
+ }
+ if ($source->hasChildNodes()) {
+ $children = $source->childNodes;
+ if ($children->length == 1) {
+ $child = $children->item(0);
+ if ($child->nodeType == XML_TEXT_NODE) {
+ $result['_value'] = $child->nodeValue;
+ return count($result) == 1 ? $result['_value'] : $result;
+ }
+ }
+ foreach ($children as $child) {
+ if ($child instanceof \DOMCharacterData) {
+ continue;
+ }
+ $result[$child->nodeName][] = $this->convertNode($child);
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Converts XML document into corresponding array.
+ *
+ * @param \DOMDocument $source
+ * @return array
+ */
+ public function convert($source)
+ {
+ return $this->convertNode($source);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Config/Mapper.php b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php
new file mode 100644
index 0000000000000..4dda8f3c733a6
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Config/Mapper.php
@@ -0,0 +1,37 @@
+readers = $readers;
+ $this->mapper = $mapper;
+ }
+
+ /**
+ * Reads configuration according to the given scope.
+ *
+ * @param string|null $scope
+ * @return array
+ */
+ public function read($scope = null)
+ {
+ $data = [];
+ foreach ($this->readers as $reader) {
+ $data = array_merge_recursive($data, $reader->read($scope));
+ }
+ return $this->mapper->execute($data);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/ConfigInterface.php b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php
new file mode 100644
index 0000000000000..ec03ddf429c06
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/ConfigInterface.php
@@ -0,0 +1,23 @@
+resourceConnection = $resourceConnection;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Creates one-time connection for export
+ *
+ * @param string $connectionName
+ * @return AdapterInterface
+ */
+ public function getConnection($connectionName)
+ {
+ $connection = $this->resourceConnection->getConnection($connectionName);
+ $connectionClassName = get_class($connection);
+ $configData = $connection->getConfig();
+ $configData['use_buffered_query'] = false;
+ unset($configData['persistent']);
+ return $this->objectManager->create(
+ $connectionClassName,
+ [
+ 'config' => $configData
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php
new file mode 100644
index 0000000000000..083b4843c185a
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/AssemblerInterface.php
@@ -0,0 +1,27 @@
+conditionResolver = $conditionResolver;
+ $this->nameResolver = $nameResolver;
+ }
+
+ /**
+ * Assembles WHERE conditions
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ if (!isset($queryConfig['source']['filter'])) {
+ return $selectBuilder;
+ }
+ $filters = $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $queryConfig['source']['filter'],
+ $this->nameResolver->getAlias($queryConfig['source'])
+ );
+ $selectBuilder->setFilters(array_merge_recursive($selectBuilder->getFilters(), [$filters]));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php
new file mode 100644
index 0000000000000..811119ace221b
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/FromAssembler.php
@@ -0,0 +1,69 @@
+nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Assembles FROM condition
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ $selectBuilder->setFrom(
+ [
+ $this->nameResolver->getAlias($queryConfig['source']) =>
+ $this->resourceConnection
+ ->getTableName($this->nameResolver->getName($queryConfig['source'])),
+ ]
+ );
+ $columns = $this->columnsResolver->getColumns($selectBuilder, $queryConfig['source']);
+ $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php
new file mode 100644
index 0000000000000..f3c6540a25171
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/Assembler/JoinAssembler.php
@@ -0,0 +1,113 @@
+conditionResolver = $conditionResolver;
+ $this->nameResolver = $nameResolver;
+ $this->columnsResolver = $columnsResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Assembles JOIN conditions
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $queryConfig
+ * @return SelectBuilder
+ */
+ public function assemble(SelectBuilder $selectBuilder, $queryConfig)
+ {
+ if (!isset($queryConfig['source']['link-source'])) {
+ return $selectBuilder;
+ }
+ $joins = [];
+ $filters = $selectBuilder->getFilters();
+
+ $sourceAlias = $this->nameResolver->getAlias($queryConfig['source']);
+
+ foreach ($queryConfig['source']['link-source'] as $join) {
+ $joinAlias = $this->nameResolver->getAlias($join);
+
+ $joins[$joinAlias] = [
+ 'link-type' => isset($join['link-type']) ? $join['link-type'] : 'left',
+ 'table' => [
+ $joinAlias => $this->resourceConnection
+ ->getTableName($this->nameResolver->getName($join)),
+ ],
+ 'condition' => $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $join['using'],
+ $joinAlias,
+ $sourceAlias
+ )
+ ];
+ if (isset($join['filter'])) {
+ $filters = array_merge(
+ $filters,
+ [
+ $this->conditionResolver->getFilter(
+ $selectBuilder,
+ $join['filter'],
+ $joinAlias,
+ $sourceAlias
+ )
+ ]
+ );
+ }
+ $columns = $this->columnsResolver->getColumns($selectBuilder, isset($join['attribute']) ? $join : []);
+ $selectBuilder->setColumns(array_merge($selectBuilder->getColumns(), $columns));
+ }
+ $selectBuilder->setFilters($filters);
+ $selectBuilder->setJoins(array_merge($selectBuilder->getJoins(), $joins));
+ return $selectBuilder;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php
new file mode 100644
index 0000000000000..e6474d4c5dc6d
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php
@@ -0,0 +1,100 @@
+nameResolver = $nameResolver;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Returns connection
+ *
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (!$this->connection) {
+ $this->connection = $this->resourceConnection->getConnection();
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Set columns list to SelectBuilder
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $entityConfig
+ * @return array
+ */
+ public function getColumns(SelectBuilder $selectBuilder, $entityConfig)
+ {
+ if (!isset($entityConfig['attribute'])) {
+ return [];
+ }
+ $group = [];
+ $columns = $selectBuilder->getColumns();
+ foreach ($entityConfig['attribute'] as $attributeData) {
+ $columnAlias = $this->nameResolver->getAlias($attributeData);
+ $tableAlias = $this->nameResolver->getAlias($entityConfig);
+ $columnName = $this->nameResolver->getName($attributeData);
+ if (isset($attributeData['function'])) {
+ $prefix = '';
+ if (isset($attributeData['distinct']) && $attributeData['distinct'] == true) {
+ $prefix = ' DISTINCT ';
+ }
+ $expression = new ColumnValueExpression(
+ strtoupper($attributeData['function']) . '(' . $prefix
+ . $this->getConnection()->quoteIdentifier($tableAlias . '.' . $columnName)
+ . ')'
+ );
+ } else {
+ $expression = $tableAlias . '.' . $columnName;
+ }
+ $columns[$columnAlias] = $expression;
+ if (isset($attributeData['group'])) {
+ $group[$columnAlias] = $expression;
+ }
+ }
+ $selectBuilder->setGroup(array_merge($selectBuilder->getGroup(), $group));
+ return $columns;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php
new file mode 100644
index 0000000000000..773b96959e794
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ConditionResolver.php
@@ -0,0 +1,166 @@
+ '%1$s = %2$s',
+ 'neq' => '%1$s != %2$s',
+ 'like' => '%1$s LIKE %2$s',
+ 'nlike' => '%1$s NOT LIKE %2$s',
+ 'in' => '%1$s IN(%2$s)',
+ 'nin' => '%1$s NOT IN(%2$s)',
+ 'notnull' => '%1$s IS NOT NULL',
+ 'null' => '%1$s IS NULL',
+ 'gt' => '%1$s > %2$s',
+ 'lt' => '%1$s < %2$s',
+ 'gteq' => '%1$s >= %2$s',
+ 'lteq' => '%1$s <= %2$s',
+ 'finset' => 'FIND_IN_SET(%2$s, %1$s)'
+ ];
+
+ /**
+ * @var \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private $connection;
+
+ /**
+ * @var ResourceConnection
+ */
+ private $resourceConnection;
+
+ /**
+ * ConditionResolver constructor.
+ * @param ResourceConnection $resourceConnection
+ */
+ public function __construct(
+ ResourceConnection $resourceConnection
+ ) {
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Returns connection
+ *
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
+ */
+ private function getConnection()
+ {
+ if (!$this->connection) {
+ $this->connection = $this->resourceConnection->getConnection();
+ }
+ return $this->connection;
+ }
+
+ /**
+ * Returns value for condition
+ *
+ * @param string $condition
+ * @param string $referencedEntity
+ * @return mixed|null|string|\Zend_Db_Expr
+ */
+ private function getValue($condition, $referencedEntity)
+ {
+ $value = null;
+ $argument = isset($condition['_value']) ? $condition['_value'] : null;
+ if (!isset($condition['type'])) {
+ $condition['type'] = 'value';
+ }
+
+ switch ($condition['type']) {
+ case "value":
+ $value = $this->getConnection()->quote($argument);
+ break;
+ case "variable":
+ $value = new Expression($argument);
+ break;
+ case "identifier":
+ $value = $this->getConnection()->quoteIdentifier(
+ $referencedEntity ? $referencedEntity . '.' . $argument : $argument
+ );
+ break;
+ }
+ return $value;
+ }
+
+ /**
+ * Returns condition for WHERE
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param string $tableName
+ * @param array $condition
+ * @param null|string $referencedEntity
+ * @return string
+ */
+ private function getCondition(SelectBuilder $selectBuilder, $tableName, $condition, $referencedEntity = null)
+ {
+ $columns = $selectBuilder->getColumns();
+ if (isset($columns[$condition['attribute']])
+ && $columns[$condition['attribute']] instanceof Expression
+ ) {
+ $expression = $columns[$condition['attribute']];
+ } else {
+ $expression = $this->getConnection()->quoteIdentifier($tableName . '.' . $condition['attribute']);
+ }
+ return sprintf(
+ $this->conditionMap[$condition['operator']],
+ $expression,
+ $this->getValue($condition, $referencedEntity)
+ );
+ }
+
+ /**
+ * Build WHERE condition
+ *
+ * @param SelectBuilder $selectBuilder
+ * @param array $filterConfig
+ * @param string $aliasName
+ * @param null|string $referencedAlias
+ * @return array
+ */
+ public function getFilter(SelectBuilder $selectBuilder, $filterConfig, $aliasName, $referencedAlias = null)
+ {
+ $filtersParts = [];
+ foreach ($filterConfig as $filter) {
+ $glue = $filter['glue'];
+ $parts = [];
+ foreach ($filter['condition'] as $condition) {
+ if (isset($condition['type']) && $condition['type'] == 'variable') {
+ $selectBuilder->setParams(array_merge($selectBuilder->getParams(), [$condition['_value']]));
+ }
+ $parts[] = $this->getCondition(
+ $selectBuilder,
+ $aliasName,
+ $condition,
+ $referencedAlias
+ );
+ }
+ if (isset($filter['filter'])) {
+ $parts[] = '(' . $this->getFilter(
+ $selectBuilder,
+ $filter['filter'],
+ $aliasName,
+ $referencedAlias
+ ) . ')';
+ }
+ $filtersParts[] = '(' . implode(' ' . strtoupper($glue) . ' ', $parts) . ')';
+ }
+ return implode(' OR ', $filtersParts);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php
new file mode 100644
index 0000000000000..c9543002eb272
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/NameResolver.php
@@ -0,0 +1,40 @@
+getName($elementConfig);
+ if (isset($elementConfig['alias'])) {
+ $alias = $elementConfig['alias'];
+ }
+ return $alias;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php b/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php
new file mode 100644
index 0000000000000..21a641f0a71c7
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/ReportValidator.php
@@ -0,0 +1,64 @@
+connectionFactory = $connectionFactory;
+ $this->queryFactory = $queryFactory;
+ }
+
+ /**
+ * Tries to do query for provided report with limit 0 and return error information if it failed
+ *
+ * @param string $name
+ * @param SearchCriteriaInterface $criteria
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function validate($name, SearchCriteriaInterface $criteria = null)
+ {
+ $query = $this->queryFactory->create($name);
+ $connection = $this->connectionFactory->getConnection($query->getConnectionName());
+ $query->getSelect()->limit(0);
+ try {
+ $connection->query($query->getSelect());
+ } catch (\Zend_Db_Statement_Exception $e) {
+ return [$name, $e->getMessage()];
+ }
+
+ return [];
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php
new file mode 100644
index 0000000000000..4e5a1940773b1
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php
@@ -0,0 +1,289 @@
+resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Get join condition
+ *
+ * @return array
+ */
+ public function getJoins()
+ {
+ return $this->joins;
+ }
+
+ /**
+ * Set joins conditions
+ *
+ * @param array $joins
+ * @return void
+ */
+ public function setJoins($joins)
+ {
+ $this->joins = $joins;
+ }
+
+ /**
+ * Get connection name
+ *
+ * @return string
+ */
+ public function getConnectionName()
+ {
+ return $this->connectionName;
+ }
+
+ /**
+ * Set connection name
+ *
+ * @param string $connectionName
+ * @return void
+ */
+ public function setConnectionName($connectionName)
+ {
+ $this->connectionName = $connectionName;
+ }
+
+ /**
+ * Get columns
+ *
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Set columns
+ *
+ * @param array $columns
+ * @return void
+ */
+ public function setColumns($columns)
+ {
+ $this->columns = $columns;
+ }
+
+ /**
+ * Get filters
+ *
+ * @return array
+ */
+ public function getFilters()
+ {
+ return $this->filters;
+ }
+
+ /**
+ * Set filters
+ *
+ * @param array $filters
+ * @return void
+ */
+ public function setFilters($filters)
+ {
+ $this->filters = $filters;
+ }
+
+ /**
+ * Get from condition
+ *
+ * @return array
+ */
+ public function getFrom()
+ {
+ return $this->from;
+ }
+
+ /**
+ * Set from condition
+ *
+ * @param array $from
+ * @return void
+ */
+ public function setFrom($from)
+ {
+ $this->from = $from;
+ }
+
+ /**
+ * Process JOIN conditions
+ *
+ * @param Select $select
+ * @param array $joinConfig
+ * @return Select
+ */
+ private function processJoin(Select $select, $joinConfig)
+ {
+ switch ($joinConfig['link-type']) {
+ case 'left':
+ $select->joinLeft($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ case 'inner':
+ $select->joinInner($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ case 'right':
+ $select->joinRight($joinConfig['table'], $joinConfig['condition'], []);
+ break;
+ }
+ return $select;
+ }
+
+ /**
+ * Creates Select object
+ *
+ * @return Select
+ */
+ public function create()
+ {
+ $connection = $this->resourceConnection->getConnection($this->getConnectionName());
+ $select = $connection->select();
+ $select->from($this->getFrom(), []);
+ $select->columns($this->getColumns());
+ foreach ($this->getFilters() as $filter) {
+ $select->where($filter);
+ }
+ foreach ($this->getJoins() as $joinConfig) {
+ $select = $this->processJoin($select, $joinConfig);
+ }
+ if (!empty($this->getGroup())) {
+ $select->group(implode(', ', $this->getGroup()));
+ }
+ return $select;
+ }
+
+ /**
+ * Returns group
+ *
+ * @return array
+ */
+ public function getGroup()
+ {
+ return $this->group;
+ }
+
+ /**
+ * Set group
+ *
+ * @param array $group
+ * @return void
+ */
+ public function setGroup($group)
+ {
+ $this->group = $group;
+ }
+
+ /**
+ * Get parameters
+ *
+ * @return array
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Set parameters
+ *
+ * @param array $params
+ * @return void
+ */
+ public function setParams($params)
+ {
+ $this->params = $params;
+ }
+
+ /**
+ * Get having condition
+ *
+ * @return array
+ */
+ public function getHaving()
+ {
+ return $this->having;
+ }
+
+ /**
+ * Set having condition
+ *
+ * @param array $having
+ * @return void
+ */
+ public function setHaving($having)
+ {
+ $this->having = $having;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php
new file mode 100644
index 0000000000000..1d88d4618efc5
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilderFactory.php
@@ -0,0 +1,43 @@
+objectManager = $objectManager;
+ }
+
+ /**
+ * Create class instance with specified parameters
+ *
+ * @param array $data
+ * @return SelectBuilder
+ */
+ public function create(array $data = [])
+ {
+ return $this->objectManager->create(SelectBuilder::class, $data);
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/IteratorFactory.php b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php
new file mode 100644
index 0000000000000..a196cef8b66dc
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/IteratorFactory.php
@@ -0,0 +1,61 @@
+objectManager = $objectManager;
+ $this->defaultIteratorName = $defaultIteratorName;
+ }
+
+ /**
+ * Creates instance of the result iterator with the query result as an input
+ * Result iterator can be changed through report configuration
+ *
+ * < ...
+ *
+ * Uses IteratorIterator by default
+ *
+ * @param \Traversable $result
+ * @param string|null $iteratorName
+ * @return \IteratorIterator
+ */
+ public function create(\Traversable $result, $iteratorName = null)
+ {
+ return $this->objectManager->create(
+ $iteratorName ?: $this->defaultIteratorName,
+ [
+ 'iterator' => $result
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/Query.php b/app/code/Magento/Analytics/ReportXml/Query.php
new file mode 100644
index 0000000000000..46ec2fb494183
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/Query.php
@@ -0,0 +1,96 @@
+select = $select;
+ $this->connectionName = $connectionName;
+ $this->selectHydrator = $selectHydrator;
+ $this->config = $config;
+ }
+
+ /**
+ * @return Select
+ */
+ public function getSelect()
+ {
+ return $this->select;
+ }
+
+ /**
+ * @return string
+ */
+ public function getConnectionName()
+ {
+ return $this->connectionName;
+ }
+
+ /**
+ * @return array
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Specify data which should be serialized to JSON
+ * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
+ * @return mixed data which can be serialized by json_encode ,
+ * which is a value of any type other than a resource.
+ * @since 5.4.0
+ */
+ public function jsonSerialize()
+ {
+ return [
+ 'connectionName' => $this->getConnectionName(),
+ 'select_parts' => $this->selectHydrator->extract($this->getSelect()),
+ 'config' => $this->getConfig()
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/QueryFactory.php b/app/code/Magento/Analytics/ReportXml/QueryFactory.php
new file mode 100644
index 0000000000000..8ed7e767b28b3
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/QueryFactory.php
@@ -0,0 +1,142 @@
+config = $config;
+ $this->selectBuilderFactory = $selectBuilderFactory;
+ $this->assemblers = $assemblers;
+ $this->queryCache = $queryCache;
+ $this->objectManager = $objectManager;
+ $this->selectHydrator = $selectHydrator;
+ }
+
+ /**
+ * Returns query connection name according to configuration
+ *
+ * @param string $queryConfig
+ * @return string
+ */
+ private function getQueryConnectionName($queryConfig)
+ {
+ $connectionName = 'default';
+ if (isset($queryConfig['connection'])) {
+ $connectionName = $queryConfig['connection'];
+ }
+ return $connectionName;
+ }
+
+ /**
+ * Create query according to configuration settings
+ *
+ * @param string $queryName
+ * @return Query
+ */
+ private function constructQuery($queryName)
+ {
+ $queryConfig = $this->config->get($queryName);
+ $selectBuilder = $this->selectBuilderFactory->create();
+ $selectBuilder->setConnectionName($this->getQueryConnectionName($queryConfig));
+ foreach ($this->assemblers as $assembler) {
+ $selectBuilder = $assembler->assemble($selectBuilder, $queryConfig);
+ }
+ $select = $selectBuilder->create();
+ return $this->objectManager->create(
+ Query::class,
+ [
+ 'select' => $select,
+ 'selectHydrator' => $this->selectHydrator,
+ 'connectionName' => $selectBuilder->getConnectionName(),
+ 'config' => $queryConfig
+ ]
+ );
+ }
+
+ /**
+ * Creates query by name
+ *
+ * @param string $queryName
+ * @return Query
+ */
+ public function create($queryName)
+ {
+ $cached = $this->queryCache->load($queryName);
+ if ($cached) {
+ $queryData = json_decode($cached, true);
+ return $this->objectManager->create(
+ Query::class,
+ [
+ 'select' => $this->selectHydrator->recreate($queryData['select_parts']),
+ 'selectHydrator' => $this->selectHydrator,
+ 'connectionName' => $queryData['connectionName'],
+ 'config' => $queryData['config']
+ ]
+ );
+ }
+ $query = $this->constructQuery($queryName);
+ $this->queryCache->save(json_encode($query), $queryName);
+ return $query;
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/ReportProvider.php b/app/code/Magento/Analytics/ReportXml/ReportProvider.php
new file mode 100644
index 0000000000000..3ebe5941108bc
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/ReportProvider.php
@@ -0,0 +1,76 @@
+queryFactory = $queryFactory;
+ $this->connectionFactory = $connectionFactory;
+ $this->iteratorFactory = $iteratorFactory;
+ }
+
+ /**
+ * Returns custom iterator name for report
+ * Null for default
+ *
+ * @param Query $query
+ * @return string|null
+ */
+ private function getIteratorName(Query $query)
+ {
+ $config = $query->getConfig();
+ return isset($config['iterator']) ? $config['iterator'] : null;
+ }
+
+ /**
+ * Returns report data by name and criteria
+ *
+ * @param string $name
+ * @return \IteratorIterator
+ */
+ public function getReport($name)
+ {
+ $query = $this->queryFactory->create($name);
+ $connection = $this->connectionFactory->getConnection($query->getConnectionName());
+ $statement = $connection->query($query->getSelect());
+ return $this->iteratorFactory->create($statement, $this->getIteratorName($query));
+ }
+}
diff --git a/app/code/Magento/Analytics/ReportXml/SelectHydrator.php b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php
new file mode 100644
index 0000000000000..02cde2fd0598d
--- /dev/null
+++ b/app/code/Magento/Analytics/ReportXml/SelectHydrator.php
@@ -0,0 +1,143 @@
+resourceConnection = $resourceConnection;
+ $this->objectManager = $objectManager;
+ $this->selectParts = $selectParts;
+ }
+
+ /**
+ * @return array
+ */
+ private function getSelectParts()
+ {
+ return array_merge($this->predefinedSelectParts, $this->selectParts);
+ }
+
+ /**
+ * Extracts Select metadata parts
+ *
+ * @param Select $select
+ * @return array
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function extract(Select $select)
+ {
+ $parts = [];
+ foreach ($this->getSelectParts() as $partName) {
+ $parts[$partName] = $select->getPart($partName);
+ }
+ return $parts;
+ }
+
+ /**
+ * @param array $selectParts
+ * @return Select
+ */
+ public function recreate(array $selectParts)
+ {
+ $select = $this->resourceConnection->getConnection()->select();
+
+ $select = $this->processColumns($select, $selectParts);
+
+ foreach ($selectParts as $partName => $partValue) {
+ $select->setPart($partName, $partValue);
+ }
+
+ return $select;
+ }
+
+ /**
+ * Process COLUMNS part values and add this part into select.
+ *
+ * If each column contains information about select expression
+ * an object with the type of this expression going to be created and assigned to this column.
+ *
+ * @param Select $select
+ * @param array $selectParts
+ * @return Select
+ */
+ private function processColumns(Select $select, array &$selectParts)
+ {
+ if (!empty($selectParts[Select::COLUMNS]) && is_array($selectParts[Select::COLUMNS])) {
+ $part = [];
+
+ foreach ($selectParts[Select::COLUMNS] as $columnEntry) {
+ list($correlationName, $column, $alias) = $columnEntry;
+ if (is_array($column) && !empty($column['class'])) {
+ $expression = $this->objectManager->create(
+ $column['class'],
+ isset($column['arguments']) ? $column['arguments'] : []
+ );
+ $part[] = [$correlationName, $expression, $alias];
+ } else {
+ $part[] = $columnEntry;
+ }
+ }
+
+ $select->setPart(Select::COLUMNS, $part);
+ unset($selectParts[Select::COLUMNS]);
+ }
+
+ return $select;
+ }
+}
diff --git a/app/code/Magento/Analytics/Setup/InstallData.php b/app/code/Magento/Analytics/Setup/InstallData.php
new file mode 100644
index 0000000000000..aaa619bbb0caa
--- /dev/null
+++ b/app/code/Magento/Analytics/Setup/InstallData.php
@@ -0,0 +1,53 @@
+getConnection()->insertMultiple(
+ $setup->getTable('core_config_data'),
+ [
+ [
+ 'scope' => 'default',
+ 'scope_id' => 0,
+ 'path' => 'analytics/subscription/enabled',
+ 'value' => 1
+ ],
+ [
+ 'scope' => 'default',
+ 'scope_id' => 0,
+ 'path' => SubscriptionHandler::CRON_STRING_PATH,
+ 'value' => join(' ', SubscriptionHandler::CRON_EXPR_ARRAY)
+ ]
+ ]
+ );
+
+ $setup->getConnection()->insert(
+ $setup->getTable('flag'),
+ [
+ 'flag_code' => SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE,
+ 'state' => 0,
+ 'flag_data' => 24,
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php
new file mode 100644
index 0000000000000..cbf06264096ac
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php
@@ -0,0 +1,77 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment', 'getLabel'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->additionalComment = $objectManager->getObject(
+ AdditionalComment::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('New comment');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getLabel')
+ ->willReturn('Comment label');
+ $html = $this->additionalComment->render($this->abstractElementMock);
+ $this->assertRegexp(
+ "/New comment/",
+ $html
+ );
+ $this->assertRegexp(
+ "/Comment label/",
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php
new file mode 100644
index 0000000000000..a652cf6b3d548
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php
@@ -0,0 +1,81 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->setMethods(['getLocaleDate'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->timeZoneMock = $this->getMockBuilder(TimezoneInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock->expects($this->any())
+ ->method('getLocaleDate')
+ ->willReturn($this->timeZoneMock);
+
+ $objectManager = new ObjectManager($this);
+ $this->collectionTimeLabel = $objectManager->getObject(
+ CollectionTimeLabel::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $timeZone = "America/New_York";
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->timeZoneMock->expects($this->once())
+ ->method('getConfigTimezone')
+ ->willReturn($timeZone);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('Eastern Standard Time (America/New_York)');
+ $this->assertRegexp(
+ "/Eastern Standard Time \(America\/New_York\)/",
+ $this->collectionTimeLabel->render($this->abstractElementMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php
new file mode 100644
index 0000000000000..09e753e4ac8aa
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php
@@ -0,0 +1,85 @@
+subscriptionStatusProviderMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->subscriptionStatusLabel = $objectManager->getObject(
+ SubscriptionStatusLabel::class,
+ [
+ 'context' => $this->contextMock,
+ 'subscriptionStatusProvider' => $this->subscriptionStatusProviderMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->subscriptionStatusProviderMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn('Enabled');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('Subscription status: Enabled');
+ $this->assertRegexp(
+ "/Subscription status: Enabled/",
+ $this->subscriptionStatusLabel->render($this->abstractElementMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php
new file mode 100644
index 0000000000000..abce48c36c86a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php
@@ -0,0 +1,77 @@
+abstractElementMock = $this->getMockBuilder(AbstractElement::class)
+ ->setMethods(['getComment', 'getLabel', 'getHint'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->formMock = $this->getMockBuilder(Form::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManager($this);
+ $this->vertical = $objectManager->getObject(
+ Vertical::class,
+ [
+ 'context' => $this->contextMock
+ ]
+ );
+ }
+
+ public function testRender()
+ {
+ $this->abstractElementMock->setForm($this->formMock);
+ $this->abstractElementMock->expects($this->any())
+ ->method('getComment')
+ ->willReturn('New comment');
+ $this->abstractElementMock->expects($this->any())
+ ->method('getHint')
+ ->willReturn('New hint');
+ $html = $this->vertical->render($this->abstractElementMock);
+ $this->assertRegexp(
+ "/New comment/",
+ $html
+ );
+ $this->assertRegExp(
+ "/New hint/",
+ $html
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php
new file mode 100644
index 0000000000000..6f613cdc4d639
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/BIEssentials/SignUpTest.php
@@ -0,0 +1,84 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultRedirectFactoryMock = $this->getMockBuilder(RedirectFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->redirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->signUpController = $this->objectManagerHelper->getObject(
+ SignUp::class,
+ [
+ 'config' => $this->configMock,
+ 'resultRedirectFactory' => $this->resultRedirectFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $urlBIEssentialsConfigPath = 'analytics/url/bi_essentials';
+ $this->configMock->expects($this->once())
+ ->method('getValue')
+ ->with($urlBIEssentialsConfigPath)
+ ->willReturn('value');
+ $this->resultRedirectFactoryMock->expects($this->once())->method('create')->willReturn($this->redirectMock);
+ $this->redirectMock->expects($this->once())->method('setUrl')->with('value')->willReturnSelf();
+ $this->assertEquals($this->redirectMock, $this->signUpController->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php
new file mode 100644
index 0000000000000..4f54ce5059965
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Reports/ShowTest.php
@@ -0,0 +1,185 @@
+reportUrlProviderMock = $this->getMockBuilder(ReportUrlProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->redirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->showController = $this->objectManagerHelper->getObject(
+ Show::class,
+ [
+ 'reportUrlProvider' => $this->reportUrlProviderMock,
+ 'resultFactory' => $this->resultFactoryMock,
+ 'messageManager' => $this->messageManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $otpUrl = 'http://example.com?otp=15vbjcfdvd15645';
+
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willReturn($otpUrl);
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setUrl')
+ ->with($otpUrl)
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+
+ /**
+ * @dataProvider executeWithExceptionDataProvider
+ *
+ * @param \Exception $exception
+ */
+ public function testExecuteWithException(\Exception $exception)
+ {
+
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willThrowException($exception);
+ if ($exception instanceof LocalizedException) {
+ $message = $exception->getMessage();
+ } else {
+ $message = __('Sorry, there has been an error processing your request. Please try again later.');
+ }
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addExceptionMessage')
+ ->with($exception, $message)
+ ->willReturnSelf();
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeWithExceptionDataProvider()
+ {
+ return [
+ 'ExecuteWithLocalizedException' => [new LocalizedException(__('TestMessage'))],
+ 'ExecuteWithException' => [new \Exception('TestMessage')],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithSubscriptionUpdateException()
+ {
+ $exception = new SubscriptionUpdateException(__('TestMessage'));
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->redirectMock);
+ $this->reportUrlProviderMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->with()
+ ->willThrowException($exception);
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addNoticeMessage')
+ ->with($exception->getMessage())
+ ->willReturnSelf();
+ $this->redirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->assertSame($this->redirectMock, $this->showController->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php
new file mode 100644
index 0000000000000..17c485a8df230
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Controller/Adminhtml/Subscription/RetryTest.php
@@ -0,0 +1,159 @@
+resultFactoryMock = $this->getMockBuilder(ResultFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resultRedirectMock = $this->getMockBuilder(Redirect::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->retryController = $this->objectManagerHelper->getObject(
+ Retry::class,
+ [
+ 'resultFactory' => $this->resultFactoryMock,
+ 'subscriptionHandler' => $this->subscriptionHandlerMock,
+ 'messageManager' => $this->messageManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecute()
+ {
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->resultRedirectMock);
+ $this->resultRedirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+ $this->assertSame(
+ $this->resultRedirectMock,
+ $this->retryController->execute()
+ );
+ }
+
+ /**
+ * @dataProvider executeExceptionsDataProvider
+ *
+ * @param \Exception $exception
+ * @param Phrase $message
+ */
+ public function testExecuteWithException(\Exception $exception, Phrase $message)
+ {
+ $this->resultFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(ResultFactory::TYPE_REDIRECT)
+ ->willReturn($this->resultRedirectMock);
+ $this->resultRedirectMock
+ ->expects($this->once())
+ ->method('setPath')
+ ->with('adminhtml')
+ ->willReturnSelf();
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willThrowException($exception);
+ $this->messageManagerMock
+ ->expects($this->once())
+ ->method('addExceptionMessage')
+ ->with($exception, $message);
+
+ $this->assertSame(
+ $this->resultRedirectMock,
+ $this->retryController->execute()
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function executeExceptionsDataProvider()
+ {
+ return [
+ [new LocalizedException(__('TestMessage')), __('TestMessage')],
+ [
+ new \Exception('TestMessage'),
+ __('Sorry, there has been an error processing your request. Please try again later.')
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php
new file mode 100644
index 0000000000000..81c57d79033c8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/CollectDataTest.php
@@ -0,0 +1,91 @@
+exportDataHandlerMock = $this->getMockBuilder(ExportDataHandlerInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->collectData = $this->objectManagerHelper->getObject(
+ CollectData::class,
+ [
+ 'exportDataHandler' => $this->exportDataHandlerMock,
+ 'subscriptionStatus' => $this->subscriptionStatusMock,
+ ]
+ );
+ }
+
+ /**
+ * @param string $status
+ * @return void
+ * @dataProvider executeDataProvider
+ */
+ public function testExecute($status)
+ {
+ $this->subscriptionStatusMock
+ ->expects($this->once())
+ ->method('getStatus')
+ ->with()
+ ->willReturn($status);
+ $this->exportDataHandlerMock
+ ->expects(($status === SubscriptionStatusProvider::ENABLED) ? $this->once() : $this->never())
+ ->method('prepareExportData')
+ ->with();
+
+ $this->assertTrue($this->collectData->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeDataProvider()
+ {
+ return [
+ 'Subscription is enabled' => [SubscriptionStatusProvider::ENABLED],
+ 'Subscription is disabled' => [SubscriptionStatusProvider::DISABLED],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php
new file mode 100644
index 0000000000000..959a11f9e1058
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/SignUpTest.php
@@ -0,0 +1,133 @@
+connectorMock = $this->getMockBuilder(Connector::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->signUp = new SignUp(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->flagManagerMock,
+ $this->reinitableConfigMock
+ );
+ }
+
+ public function testExecute()
+ {
+ $attemptsCount = 10;
+
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($attemptsCount);
+
+ $attemptsCount -= 1;
+ $this->flagManagerMock->expects($this->once())
+ ->method('saveFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $attemptsCount);
+ $this->connectorMock->expects($this->once())
+ ->method('execute')
+ ->with('signUp')
+ ->willReturn(true);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->flagManagerMock->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ $this->assertTrue($this->signUp->execute());
+ }
+
+ public function testExecuteFlagNotExist()
+ {
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(null);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->assertFalse($this->signUp->execute());
+ }
+
+ public function testExecuteZeroAttempts()
+ {
+ $attemptsCount = 0;
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($attemptsCount);
+ $this->addDeleteAnalyticsCronExprAsserts();
+ $this->flagManagerMock->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE);
+ $this->assertFalse($this->signUp->execute());
+ }
+
+ /**
+ * Add assertions for method deleteAnalyticsCronExpr.
+ *
+ * @return void
+ */
+ private function addDeleteAnalyticsCronExprAsserts()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(SubscriptionHandler::CRON_STRING_PATH)
+ ->willReturn(true);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->willReturnSelf();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php
new file mode 100644
index 0000000000000..ede53d8783a7a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Cron/UpdateTest.php
@@ -0,0 +1,214 @@
+connectorMock = $this->getMockBuilder(Connector::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->update = new Update(
+ $this->connectorMock,
+ $this->configWriterMock,
+ $this->reinitableConfigMock,
+ $this->flagManagerMock,
+ $this->analyticsTokenMock
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testExecuteWithoutToken()
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(10);
+ $this->connectorMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(false);
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->addFinalOutputAsserts();
+ $this->assertFalse($this->update->execute());
+ }
+
+ /**
+ * @param bool $isExecuted
+ */
+ private function addFinalOutputAsserts(bool $isExecuted = true)
+ {
+ $this->flagManagerMock
+ ->expects($this->exactly(2 * $isExecuted))
+ ->method('deleteFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE],
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE]
+ );
+ $this->configWriterMock
+ ->expects($this->exactly((int)$isExecuted))
+ ->method('delete')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH);
+ $this->reinitableConfigMock
+ ->expects($this->exactly((int)$isExecuted))
+ ->method('reinit')
+ ->with();
+ }
+
+ /**
+ * @param $counterData
+ * @return void
+ * @dataProvider executeWithEmptyReverseCounterDataProvider
+ */
+ public function testExecuteWithEmptyReverseCounter($counterData)
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($counterData);
+ $this->connectorMock
+ ->expects($this->never())
+ ->method('execute')
+ ->with('update')
+ ->willReturn(false);
+ $this->analyticsTokenMock
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->addFinalOutputAsserts();
+ $this->assertFalse($this->update->execute());
+ }
+
+ /**
+ * Provides empty states of the reverse counter.
+ *
+ * @return array
+ */
+ public function executeWithEmptyReverseCounterDataProvider()
+ {
+ return [
+ [null],
+ [0]
+ ];
+ }
+
+ /**
+ * @param int $reverseCount
+ * @param bool $commandResult
+ * @param bool $finalConditionsIsExpected
+ * @param bool $functionResult
+ * @return void
+ * @dataProvider executeRegularScenarioDataProvider
+ */
+ public function testExecuteRegularScenario(
+ int $reverseCount,
+ bool $commandResult,
+ bool $finalConditionsIsExpected,
+ bool $functionResult
+ ) {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn($reverseCount);
+ $this->connectorMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with('update')
+ ->willReturn($commandResult);
+ $this->analyticsTokenMock
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->addFinalOutputAsserts($finalConditionsIsExpected);
+ $this->assertSame($functionResult, $this->update->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeRegularScenarioDataProvider()
+ {
+ return [
+ 'The last attempt with command execution result False' => [
+ 'Reverse count' => 1,
+ 'Command result' => false,
+ 'Executed final output conditions' => true,
+ 'Function result' => false,
+ ],
+ 'Not the last attempt with command execution result False' => [
+ 'Reverse count' => 10,
+ 'Command result' => false,
+ 'Executed final output conditions' => false,
+ 'Function result' => false,
+ ],
+ 'Command execution result True' => [
+ 'Reverse count' => 10,
+ 'Command result' => true,
+ 'Executed final output conditions' => true,
+ 'Function result' => true,
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php
new file mode 100644
index 0000000000000..57315543bc32d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/AnalyticsTokenTest.php
@@ -0,0 +1,129 @@
+reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->tokenModel = $this->objectManagerHelper->getObject(
+ AnalyticsToken::class,
+ [
+ 'reinitableConfig' => $this->reinitableConfigMock,
+ 'config' => $this->configMock,
+ 'configWriter' => $this->configWriterMock,
+ 'tokenPath' => $this->tokenPath,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testStoreToken()
+ {
+ $value = 'jjjj0000';
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with($this->tokenPath, $value);
+
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->willReturnSelf();
+
+ $this->assertTrue($this->tokenModel->storeToken($value));
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetToken()
+ {
+ $value = 'jjjj0000';
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->tokenPath)
+ ->willReturn($value);
+
+ $this->assertSame($value, $this->tokenModel->getToken());
+ }
+
+ /**
+ * @return void
+ */
+ public function testIsTokenExist()
+ {
+ $this->assertFalse($this->tokenModel->isTokenExist());
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->tokenPath)
+ ->willReturn('0000');
+ $this->assertTrue($this->tokenModel->isTokenExist());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php
new file mode 100644
index 0000000000000..f5f721c038c57
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Baseurl/SubscriptionUpdateHandlerTest.php
@@ -0,0 +1,178 @@
+reinitableConfigMock = $this->getMockBuilder(ReinitableConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->subscriptionUpdateHandler = $this->objectManagerHelper->getObject(
+ SubscriptionUpdateHandler::class,
+ [
+ 'reinitableConfig' => $this->reinitableConfigMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'flagManager' => $this->flagManagerMock,
+ 'configWriter' => $this->configWriterMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenDoesNotExist()
+ {
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('saveFlag');
+ $this->configWriterMock
+ ->expects($this->never())
+ ->method('save');
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate('http://store.com'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenAndPreviousBaseUrlExist()
+ {
+ $url = 'https://store.com';
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue],
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url]
+ );
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->with();
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url));
+ }
+
+ /**
+ * @return void
+ */
+ public function testTokenExistAndWithoutPreviousBaseUrl()
+ {
+ $url = 'https://store.com';
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->exactly(2))
+ ->method('saveFlag')
+ ->withConsecutive(
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, $url],
+ [SubscriptionUpdateHandler::SUBSCRIPTION_UPDATE_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue]
+ );
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionUpdateHandler::UPDATE_CRON_STRING_PATH, $this->cronExpression);
+ $this->reinitableConfigMock
+ ->expects($this->once())
+ ->method('reinit')
+ ->with();
+ $this->assertTrue($this->subscriptionUpdateHandler->processUrlUpdate($url));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php
new file mode 100644
index 0000000000000..071b96111ac8b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/CollectionTimeTest.php
@@ -0,0 +1,111 @@
+configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->collectionTime = $this->objectManagerHelper->getObject(
+ CollectionTime::class,
+ [
+ 'configWriter' => $this->configWriterMock,
+ '_logger' => $this->loggerMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSave()
+ {
+ $this->collectionTime->setData('value', '05,04,03');
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH, join(' ', ['04', '05', '*', '*', '*']));
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->collectionTime->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testAfterSaveWrongValue()
+ {
+ $this->collectionTime->setData('value', '00,01');
+ $this->collectionTime->afterSave();
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testAfterSaveWithLocalizedException()
+ {
+ $exception = new \Exception('Test message');
+ $this->collectionTime->setData('value', '05,04,03');
+
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH, join(' ', ['04', '05', '*', '*', '*']))
+ ->willThrowException($exception);
+ $this->loggerMock
+ ->expects($this->once())
+ ->method('error')
+ ->with($exception->getMessage());
+ $this->collectionTime->afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php
new file mode 100644
index 0000000000000..82aa4dc72dfe0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/Enabled/SubscriptionHandlerTest.php
@@ -0,0 +1,152 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->tokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->subscriptionHandler = $this->objectManagerHelper->getObject(
+ SubscriptionHandler::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ 'configWriter' => $this->configWriterMock,
+ 'attemptsInitValue' => $this->attemptsInitValue,
+ 'analyticsToken' => $this->tokenMock,
+ ]
+ );
+ }
+
+ public function testProcessEnabledTokenExist()
+ {
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->configWriterMock
+ ->expects($this->never())
+ ->method('save');
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('saveFlag');
+ $this->assertTrue(
+ $this->subscriptionHandler->processEnabled()
+ );
+ }
+
+ public function testProcessEnabledTokenDoesNotExist()
+ {
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('save')
+ ->with(SubscriptionHandler::CRON_STRING_PATH, "0 * * * *");
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, $this->attemptsInitValue)
+ ->willReturn(true);
+ $this->assertTrue(
+ $this->subscriptionHandler->processEnabled()
+ );
+ }
+
+ public function testProcessDisabledTokenDoesNotExist()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH);
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('deleteFlag')
+ ->with(SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE)
+ ->willReturn(true);
+ $this->assertTrue(
+ $this->subscriptionHandler->processDisabled()
+ );
+ }
+
+ public function testProcessDisabledTokenExists()
+ {
+ $this->configWriterMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with(CollectionTime::CRON_SCHEDULE_PATH);
+ $this->tokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->flagManagerMock
+ ->expects($this->never())
+ ->method('deleteFlag');
+ $this->assertTrue(
+ $this->subscriptionHandler->processDisabled()
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php
new file mode 100644
index 0000000000000..eea3193258bc6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/EnabledTest.php
@@ -0,0 +1,184 @@
+subscriptionHandlerMock = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->enabledModel = $this->objectManagerHelper->getObject(
+ Enabled::class,
+ [
+ 'subscriptionHandler' => $this->subscriptionHandlerMock,
+ '_logger' => $this->loggerMock,
+ 'config' => $this->configMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessEnabled()
+ {
+ $this->enabledModel->setData('value', $this->valueEnabled);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(!$this->valueEnabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessDisabled()
+ {
+ $this->enabledModel->setData('value', $this->valueDisabled);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(!$this->valueDisabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processDisabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSaveSuccessValueNotChanged()
+ {
+ $this->enabledModel->setData('value', null);
+
+ $this->configMock
+ ->expects($this->any())
+ ->method('getValue')
+ ->willReturn(null);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->never())
+ ->method('processEnabled')
+ ->with()
+ ->willReturn(true);
+ $this->subscriptionHandlerMock
+ ->expects($this->never())
+ ->method('processDisabled')
+ ->with()
+ ->willReturn(true);
+
+ $this->assertInstanceOf(
+ Value::class,
+ $this->enabledModel->afterSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testExecuteAfterSaveFailedWithLocalizedException()
+ {
+ $exception = new \Exception('Message');
+ $this->enabledModel->setData('value', $this->valueEnabled);
+
+ $this->subscriptionHandlerMock
+ ->expects($this->once())
+ ->method('processEnabled')
+ ->with()
+ ->willThrowException($exception);
+
+ $this->loggerMock
+ ->expects($this->once())
+ ->method('error')
+ ->with($exception->getMessage());
+
+ $this->enabledModel->afterSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php
new file mode 100644
index 0000000000000..6fe7d0aa93998
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Backend/VerticalTest.php
@@ -0,0 +1,59 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Config\Backend\Vertical::class
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testBeforeSaveSuccess()
+ {
+ $this->subject->setValue('Apps and Games');
+
+ $this->assertInstanceOf(
+ \Magento\Analytics\Model\Config\Backend\Vertical::class,
+ $this->subject->beforeSave()
+ );
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testBeforeSaveFailedWithLocalizedException()
+ {
+ $this->subject->setValue('');
+
+ $this->subject->beforeSave();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php
new file mode 100644
index 0000000000000..0b7f4870dbac8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/MapperTest.php
@@ -0,0 +1,142 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->mapper = $this->objectManagerHelper->getObject(Mapper::class);
+ }
+
+ /**
+ * @param array $configData
+ * @param array $resultData
+ * @return void
+ *
+ * @dataProvider executingDataProvider
+ */
+ public function testExecution($configData, $resultData)
+ {
+ $this->assertSame($resultData, $this->mapper->execute($configData));
+ }
+
+ /**
+ * @return array
+ */
+ public function executingDataProvider()
+ {
+ return [
+ 'wrongConfig' => [
+ ['config' => ['files']],
+ []
+ ],
+ 'validConfigWithFileNodes' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [[]]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => []
+ ]
+ ],
+ ],
+ 'validConfigWithProvidersNode' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 0 => [
+ 'reportProvider' => [0 => []]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 'reportProvider' => ['parameters' => []]
+ ]
+ ]
+ ],
+ ],
+ 'validConfigWithParametersNode' => [
+ [
+ 'config' => [
+ 0 => [
+ 'file' => [
+ 0 => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 0 => [
+ 'reportProvider' => [
+ 0 => [
+ 'parameters' => [
+ 0 => ['name' => ['reportName']]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'fileName' => [
+ 'name' => 'fileName',
+ 'providers' => [
+ 'reportProvider' => [
+ 'parameters' => [
+ 'name' => 'reportName'
+ ]
+ ]
+ ]
+ ]
+ ],
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php
new file mode 100644
index 0000000000000..6aa9c7ef3106c
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/ReaderTest.php
@@ -0,0 +1,108 @@
+mapperMock = $this->getMockBuilder(Mapper::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->readerXmlMock = $this->getMockBuilder(ReaderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->readerDbMock = $this->getMockBuilder(ReaderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reader = $this->objectManagerHelper->getObject(
+ Reader::class,
+ [
+ 'mapper' => $this->mapperMock,
+ 'readers' => [
+ $this->readerXmlMock,
+ $this->readerDbMock,
+ ],
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testRead()
+ {
+ $scope = 'store';
+ $xmlReaderResult = [
+ 'config' => ['node1' => ['node2' => 'node4']]
+ ];
+ $dbReaderResult = [
+ 'config' => ['node1' => ['node2' => 'node3']]
+ ];
+ $mapperResult = ['node2' => ['node3', 'node4']];
+
+ $this->readerXmlMock
+ ->expects($this->once())
+ ->method('read')
+ ->with($scope)
+ ->willReturn($xmlReaderResult);
+
+ $this->readerDbMock
+ ->expects($this->once())
+ ->method('read')
+ ->with($scope)
+ ->willReturn($dbReaderResult);
+
+ $this->mapperMock
+ ->expects($this->once())
+ ->method('execute')
+ ->with(array_merge_recursive($xmlReaderResult, $dbReaderResult))
+ ->willReturn($mapperResult);
+
+ $this->assertSame($mapperResult, $this->reader->read($scope));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php
new file mode 100644
index 0000000000000..c13205d34f25b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Config/Source/VerticalTest.php
@@ -0,0 +1,60 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Config\Source\Vertical::class,
+ [
+ 'verticals' => [
+ 'Apps and Games',
+ 'Athletic/Sporting Goods',
+ 'Art and Design'
+ ]
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testToOptionArray()
+ {
+ $expectedOptionsArray = [
+ ['value' => '', 'label' => __('--Please Select--')],
+ ['value' => 'Apps and Games', 'label' => __('Apps and Games')],
+ ['value' => 'Athletic/Sporting Goods', 'label' => __('Athletic/Sporting Goods')],
+ ['value' => 'Art and Design', 'label' => __('Art and Design')]
+ ];
+
+ $this->assertEquals(
+ $expectedOptionsArray,
+ $this->subject->toOptionArray()
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php
new file mode 100644
index 0000000000000..8739219ebdf09
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ConfigTest.php
@@ -0,0 +1,68 @@
+dataInterfaceMock = $this->getMockBuilder(DataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->config = $this->objectManagerHelper->getObject(
+ Config::class,
+ [
+ 'data' => $this->dataInterfaceMock,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGet()
+ {
+ $key = 'configKey';
+ $defaultValue = 'mock';
+ $configValue = 'emptyString';
+
+ $this->dataInterfaceMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($key, $defaultValue)
+ ->willReturn($configValue);
+
+ $this->assertSame($configValue, $this->config->get($key, $defaultValue));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php
new file mode 100644
index 0000000000000..f8f3919b2489e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/Client/CurlTest.php
@@ -0,0 +1,218 @@
+curlMock = $this->getMockBuilder(
+ \Magento\Framework\HTTP\Adapter\Curl::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(
+ \Psr\Log\LoggerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->curlFactoryMock = $this->getMockBuilder(CurlFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->curlFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->curlMock);
+
+ $this->responseFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\Model\Connector\Http\ResponseFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->converterMock = $this->createJsonConverter();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\Model\Connector\Http\Client\Curl::class,
+ [
+ 'curlFactory' => $this->curlFactoryMock,
+ 'responseFactory' => $this->responseFactoryMock,
+ 'converter' => $this->converterMock,
+ 'logger' => $this->loggerMock,
+ ]
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ public function getTestData()
+ {
+ return [
+ [
+ 'data' => [
+ 'version' => '1.1',
+ 'body'=> ['name' => 'value'],
+ 'url' => 'http://www.mystore.com',
+ 'headers' => [JsonConverter::CONTENT_TYPE_HEADER],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestSuccess(array $data)
+ {
+ $responseString = 'This is response.';
+ $response = new \Zend_Http_Response(201, [], $responseString);
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ json_encode($data['body'])
+ );
+ $this->curlMock->expects($this->once())
+ ->method('read')
+ ->willReturn($responseString);
+ $this->curlMock->expects($this->any())
+ ->method('getErrno')
+ ->willReturn(0);
+
+ $this->responseFactoryMock->expects($this->any())
+ ->method('create')
+ ->with($responseString)
+ ->willReturn($response);
+
+ $this->assertEquals(
+ $response,
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+
+ /**
+ * @return void
+ * @dataProvider getTestData
+ */
+ public function testRequestError(array $data)
+ {
+ $response = new \Zend_Http_Response(0, []);
+ $this->curlMock->expects($this->once())
+ ->method('write')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['version'],
+ $data['headers'],
+ json_encode($data['body'])
+ );
+ $this->curlMock->expects($this->once())
+ ->method('read');
+ $this->curlMock->expects($this->atLeastOnce())
+ ->method('getErrno')
+ ->willReturn(1);
+ $this->curlMock->expects($this->atLeastOnce())
+ ->method('getError')
+ ->willReturn('CURL error.');
+
+ $this->loggerMock->expects($this->once())
+ ->method('critical')
+ ->with(
+ new \Exception(
+ 'MBI service CURL connection error #1: CURL error.'
+ )
+ );
+
+ $this->assertEquals(
+ $response,
+ $this->subject->request(
+ $data['method'],
+ $data['url'],
+ $data['body'],
+ $data['headers'],
+ $data['version']
+ )
+ );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createJsonConverter()
+ {
+ $converterMock = $this->getMockBuilder(ConverterInterface::class)
+ ->getMockForAbstractClass();
+ $converterMock->expects($this->any())->method('toBody')->willReturnCallback(function ($value) {
+ return json_encode($value);
+ });
+ $converterMock->expects($this->any())
+ ->method('getContentTypeHeader')
+ ->willReturn(JsonConverter::CONTENT_TYPE_HEADER);
+ return $converterMock;
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php
new file mode 100644
index 0000000000000..60a19f3d5079e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/JsonConverterTest.php
@@ -0,0 +1,34 @@
+assertEquals(JsonConverter::CONTENT_TYPE_HEADER, $converter->getContentTypeHeader());
+ }
+
+ public function testConvertBody()
+ {
+ $body = '{"token": "secret-token"}';
+ $converter = new JsonConverter();
+ $this->assertEquals(json_decode($body, 1), $converter->fromBody($body));
+ }
+
+ public function testConvertData()
+ {
+ $data = ["token" => "secret-token"];
+ $converter = new JsonConverter();
+ $this->assertEquals(json_encode($data), $converter->toBody($data));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php
new file mode 100644
index 0000000000000..7c3c484843285
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/Http/ResponseResolverTest.php
@@ -0,0 +1,39 @@
+ 'testValue'];
+ $response = new \Zend_Http_Response(201, [], json_encode($expectedBody));
+ $responseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $responseHandlerMock->expects($this->once())
+ ->method('handleResponse')
+ ->with($expectedBody)
+ ->willReturn(true);
+ $notFoundResponseHandlerMock = $this->getMockBuilder(ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $notFoundResponseHandlerMock->expects($this->never())->method('handleResponse');
+ $responseResolver = new ResponseResolver(
+ new JsonConverter(),
+ [
+ 201 => $responseHandlerMock,
+ 404 => $notFoundResponseHandlerMock,
+ ]
+ );
+ $this->assertTrue($responseResolver->getResult($response));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php
new file mode 100644
index 0000000000000..cee3877631c2e
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/NotifyDataChangedCommandTest.php
@@ -0,0 +1,107 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $successHandler = $this->getMockBuilder(\Magento\Analytics\Model\Connector\Http\ResponseHandlerInterface::class)
+ ->getMockForAbstractClass();
+ $successHandler->method('handleResponse')
+ ->willReturn(true);
+
+ $this->notifyDataChangedCommand = new NotifyDataChangedCommand(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ new ResponseResolver(new JsonConverter(), [201 => $successHandler]),
+ $this->loggerMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $configVal = "Config val";
+ $token = "Secret token!";
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($configVal);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ ZendClient::POST,
+ $configVal,
+ ['access-token' => $token, 'url' => $configVal]
+ )->willReturn(new \Zend_Http_Response(201, []));
+ $this->assertTrue($this->notifyDataChangedCommand->execute());
+ }
+
+ public function testExecuteWithoutToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->httpClientMock->expects($this->never())
+ ->method('request');
+ $this->assertFalse($this->notifyDataChangedCommand->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php
new file mode 100644
index 0000000000000..8a3f4efb15cf4
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/OTPRequestTest.php
@@ -0,0 +1,187 @@
+loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subject = new OTPRequest(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ $this->responseResolverMock,
+ $this->loggerMock
+ );
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'otp' => 'thisisotp',
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ 'body'=> ['access-token' => 'thisisaccesstoken','url' => 'http://www.mystore.com'],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallSuccess()
+ {
+ $data = $this->getTestData();
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($data['access-token']);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn(new \Zend_Http_Response(201, []));
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn($data['otp']);
+
+ $this->assertEquals(
+ $data['otp'],
+ $this->subject->call()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallNoAccessToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+
+ $this->httpClientMock->expects($this->never())
+ ->method('request');
+
+ $this->assertFalse($this->subject->call());
+ }
+
+ /**
+ * @return void
+ */
+ public function testCallNoOtp()
+ {
+ $data = $this->getTestData();
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($data['access-token']);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn(new \Zend_Http_Response(0, []));
+
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn(false);
+
+ $this->loggerMock->expects($this->once())
+ ->method('warning');
+
+ $this->assertFalse($this->subject->call());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php
new file mode 100644
index 0000000000000..0ff36cca5db2d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/OTPTest.php
@@ -0,0 +1,22 @@
+assertFalse($OTPHandler->handleResponse([]));
+ $expectedOtp = 123;
+ $this->assertEquals($expectedOtp, $OTPHandler->handleResponse(['otp' => $expectedOtp]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php
new file mode 100644
index 0000000000000..707003149bcfd
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/ReSignUpTest.php
@@ -0,0 +1,36 @@
+getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $analyticsToken->expects($this->once())
+ ->method('storeToken')
+ ->with(null);
+ $subscriptionHandler = $this->getMockBuilder(SubscriptionHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $subscriptionStatusProvider = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $subscriptionStatusProvider->method('getStatus')->willReturn(SubscriptionStatusProvider::ENABLED);
+ $reSignUpHandler = new ReSignUp($analyticsToken, $subscriptionHandler, $subscriptionStatusProvider);
+ $this->assertFalse($reSignUpHandler->handleResponse([]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php
new file mode 100644
index 0000000000000..81711cfc56950
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/SignUpTest.php
@@ -0,0 +1,30 @@
+getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $analyticsToken->expects($this->once())
+ ->method('storeToken')
+ ->with($accessToken);
+ $signUpHandler = new SignUp($analyticsToken, new JsonConverter());
+ $this->assertFalse($signUpHandler->handleResponse([]));
+ $this->assertEquals($accessToken, $signUpHandler->handleResponse(['access-token' => $accessToken]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php
new file mode 100644
index 0000000000000..7779357e8bea7
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/ResponseHandler/UpdateTest.php
@@ -0,0 +1,20 @@
+assertTrue($updateHandler->handleResponse([]));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php
new file mode 100644
index 0000000000000..5593496a957b7
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/SignUpCommandTest.php
@@ -0,0 +1,174 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationManagerMock = $this->getMockBuilder(IntegrationManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->integrationToken = $this->getMockBuilder(IntegrationToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->signUpCommand = new SignUpCommand(
+ $this->analyticsTokenMock,
+ $this->integrationManagerMock,
+ $this->configMock,
+ $this->httpClientMock,
+ $this->loggerMock,
+ $this->responseResolverMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn($this->integrationToken);
+ $this->integrationManagerMock->expects($this->once())
+ ->method('activateIntegration')
+ ->willReturn(true);
+ $data = $this->getTestData();
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($data['url']);
+ $this->integrationToken->expects($this->any())
+ ->method('getData')
+ ->with('token')
+ ->willReturn($data['integration-token']);
+ $httpResponse = new \Zend_Http_Response(201, [], '{"access-token": "' . $data['access-token'] . '"}');
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ $data['method'],
+ $data['url'],
+ $data['body']
+ )
+ ->willReturn($httpResponse);
+ $this->responseResolverMock->expects($this->any())
+ ->method('getResult')
+ ->with($httpResponse)
+ ->willReturn(true);
+ $this->assertTrue($this->signUpCommand->execute());
+ }
+
+ public function testExecuteFailureCannotGenerateToken()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn(false);
+ $this->integrationManagerMock->expects($this->never())
+ ->method('activateIntegration');
+ $this->assertFalse($this->signUpCommand->execute());
+ }
+
+ public function testExecuteFailureResponseIsEmpty()
+ {
+ $this->integrationManagerMock->expects($this->once())
+ ->method('generateToken')
+ ->willReturn($this->integrationToken);
+ $this->integrationManagerMock->expects($this->once())
+ ->method('activateIntegration')
+ ->willReturn(true);
+ $httpResponse = new \Zend_Http_Response(0, []);
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->willReturn($httpResponse);
+ $this->responseResolverMock->expects($this->any())
+ ->method('getResult')
+ ->willReturn(false);
+ $this->assertFalse($this->signUpCommand->execute());
+ }
+
+ /**
+ * Returns test parameters for request.
+ *
+ * @return array
+ */
+ private function getTestData()
+ {
+ return [
+ 'url' => 'http://www.mystore.com',
+ 'access-token' => 'thisisaccesstoken',
+ 'integration-token' => 'thisisintegrationtoken',
+ 'headers' => [JsonConverter::CONTENT_TYPE_HEADER],
+ 'method' => \Magento\Framework\HTTP\ZendClient::POST,
+ 'body'=> ['token' => 'thisisintegrationtoken','url' => 'http://www.mystore.com'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php
new file mode 100644
index 0000000000000..47253a13530e5
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Connector/UpdateCommandTest.php
@@ -0,0 +1,143 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->httpClientMock = $this->getMockBuilder(ClientInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->responseResolverMock = $this->getMockBuilder(ResponseResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->updateCommand = new UpdateCommand(
+ $this->analyticsTokenMock,
+ $this->httpClientMock,
+ $this->configMock,
+ $this->loggerMock,
+ $this->flagManagerMock,
+ $this->responseResolverMock
+ );
+ }
+
+ public function testExecuteSuccess()
+ {
+ $url = "old.localhost.com";
+ $configVal = "Config val";
+ $token = "Secret token!";
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+
+ $this->configMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($configVal);
+
+ $this->flagManagerMock->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn($url);
+
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('getToken')
+ ->willReturn($token);
+
+ $this->httpClientMock->expects($this->once())
+ ->method('request')
+ ->with(
+ ZendClient::PUT,
+ $configVal,
+ [
+ 'url' => $url,
+ 'new-url' => $configVal,
+ 'access-token' => $token
+ ]
+ )->willReturn(new \Zend_Http_Response(200, []));
+
+ $this->responseResolverMock->expects($this->once())
+ ->method('getResult')
+ ->willReturn(true);
+
+ $this->assertTrue($this->updateCommand->execute());
+ }
+
+ public function testExecuteWithoutToken()
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+
+ $this->assertFalse($this->updateCommand->execute());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php
new file mode 100644
index 0000000000000..4414b81cbc183
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ConnectorTest.php
@@ -0,0 +1,70 @@
+objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->signUpCommandMock = $this->getMockBuilder(SignUpCommand::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->commands = ['signUp' => SignUpCommand::class];
+ $this->connector = new Connector($this->commands, $this->objectManagerMock);
+ }
+
+ public function testExecute()
+ {
+ $commandName = 'signUp';
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with($this->commands[$commandName])
+ ->willReturn($this->signUpCommandMock);
+ $this->signUpCommandMock->expects($this->once())
+ ->method('execute')
+ ->willReturn(true);
+ $this->assertTrue($this->connector->execute($commandName));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\NotFoundException
+ */
+ public function testExecuteCommandNotFound()
+ {
+ $commandName = 'register';
+ $this->connector->execute($commandName);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php
new file mode 100644
index 0000000000000..a896c309b4007
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/CryptographerTest.php
@@ -0,0 +1,226 @@
+analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextFactoryMock = $this->getMockBuilder(EncodedContextFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->key = '';
+ $this->source = '';
+ $this->initializationVectors = [];
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->cryptographer = $this->objectManagerHelper->getObject(
+ Cryptographer::class,
+ [
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'encodedContextFactory' => $this->encodedContextFactoryMock,
+ 'cipherMethod' => $this->cipherMethod,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testEncode()
+ {
+ $token = 'some-token-value';
+ $this->source = 'Some text';
+ $this->key = hash('sha256', $token);
+
+ $checkEncodedContext = function ($parameters) {
+ $emptyRequiredParameters =
+ array_diff(['content', 'initializationVector'], array_keys(array_filter($parameters)));
+ if ($emptyRequiredParameters) {
+ return false;
+ }
+
+ $encryptedData = openssl_encrypt(
+ $this->source,
+ $this->cipherMethod,
+ $this->key,
+ OPENSSL_RAW_DATA,
+ $parameters['initializationVector']
+ );
+
+ return ($encryptedData === $parameters['content']);
+ };
+
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('getToken')
+ ->with()
+ ->willReturn($token);
+
+ $this->encodedContextFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->callback($checkEncodedContext))
+ ->willReturn($this->encodedContextMock);
+
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ }
+
+ /**
+ * @return void
+ */
+ public function testEncodeUniqueInitializationVector()
+ {
+ $this->source = 'Some text';
+ $token = 'some-token-value';
+
+ $registerInitializationVector = function ($parameters) {
+ if (empty($parameters['initializationVector'])) {
+ return false;
+ }
+
+ $this->initializationVectors[] = $parameters['initializationVector'];
+
+ return true;
+ };
+
+ $this->analyticsTokenMock
+ ->expects($this->exactly(2))
+ ->method('getToken')
+ ->with()
+ ->willReturn($token);
+
+ $this->encodedContextFactoryMock
+ ->expects($this->exactly(2))
+ ->method('create')
+ ->with($this->callback($registerInitializationVector))
+ ->willReturn($this->encodedContextMock);
+
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ $this->assertSame($this->encodedContextMock, $this->cryptographer->encode($this->source));
+ $this->assertCount(2, array_unique($this->initializationVectors));
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ * @dataProvider encodeNotValidSourceDataProvider
+ */
+ public function testEncodeNotValidSource($source)
+ {
+ $this->cryptographer->encode($source);
+ }
+
+ /**
+ * @return array
+ */
+ public function encodeNotValidSourceDataProvider()
+ {
+ return [
+ 'Array' => [[]],
+ 'Empty string' => [''],
+ ];
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testEncodeNotValidCipherMethod()
+ {
+ $source = 'Some string';
+ $cryptographer = $this->objectManagerHelper->getObject(
+ Cryptographer::class,
+ [
+ 'cipherMethod' => 'Wrong-method',
+ ]
+ );
+
+ $cryptographer->encode($source);
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testEncodeTokenNotValid()
+ {
+ $source = 'Some string';
+
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('getToken')
+ ->with()
+ ->willReturn(null);
+
+ $this->cryptographer->encode($source);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php b/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php
new file mode 100644
index 0000000000000..a1a7c54510681
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/EncodedContextTest.php
@@ -0,0 +1,61 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @param string $content
+ * @param string|null $initializationVector
+ * @return void
+ * @dataProvider constructDataProvider
+ */
+ public function testConstruct($content, $initializationVector)
+ {
+ $constructorArguments = [
+ 'content' => $content,
+ 'initializationVector' => $initializationVector,
+ ];
+ /** @var EncodedContext $encodedContext */
+ $encodedContext = $this->objectManagerHelper->getObject(
+ EncodedContext::class,
+ array_filter($constructorArguments)
+ );
+
+ $this->assertSame($content, $encodedContext->getContent());
+ $this->assertSame($initializationVector ?: '', $encodedContext->getInitializationVector());
+ }
+
+ /**
+ * @return array
+ */
+ public function constructDataProvider()
+ {
+ return [
+ 'Without Initialization Vector' => ['content text', null],
+ 'With Initialization Vector' => ['content text', 'c51sd3c4sd68c5sd'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php
new file mode 100644
index 0000000000000..1582c241bf45d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerNotificationTest.php
@@ -0,0 +1,74 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @return void
+ */
+ public function testThatNotifyExecuted()
+ {
+ $expectedResult = true;
+ $notifyCommandName = 'notifyDataChanged';
+ $exportDataHandlerMockObject = $this->createExportDataHandlerMock();
+ $analyticsConnectorMockObject = $this->createAnalyticsConnectorMock();
+ /**
+ * @var $exportDataHandlerNotification ExportDataHandlerNotification
+ */
+ $exportDataHandlerNotification = $this->objectManagerHelper->getObject(
+ ExportDataHandlerNotification::class,
+ [
+ 'exportDataHandler' => $exportDataHandlerMockObject,
+ 'connector' => $analyticsConnectorMockObject,
+ ]
+ );
+ $exportDataHandlerMockObject->expects($this->once())
+ ->method('prepareExportData')
+ ->willReturn($expectedResult);
+ $analyticsConnectorMockObject->expects($this->once())
+ ->method('execute')
+ ->with($notifyCommandName);
+ $this->assertEquals($expectedResult, $exportDataHandlerNotification->prepareExportData());
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createExportDataHandlerMock()
+ {
+ return $this->getMockBuilder(ExportDataHandler::class)->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createAnalyticsConnectorMock()
+ {
+ return $this->getMockBuilder(Connector::class)->disableOriginalConstructor()->getMock();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php
new file mode 100644
index 0000000000000..6ffb61d088567
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php
@@ -0,0 +1,270 @@
+filesystemMock = $this->getMockBuilder(Filesystem::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->archiveMock = $this->getMockBuilder(Archive::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->reportWriterMock = $this->getMockBuilder(ReportWriterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->cryptographerMock = $this->getMockBuilder(Cryptographer::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileRecorderMock = $this->getMockBuilder(FileRecorder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->exportDataHandler = $this->objectManagerHelper->getObject(
+ ExportDataHandler::class,
+ [
+ 'filesystem' => $this->filesystemMock,
+ 'archive' => $this->archiveMock,
+ 'reportWriter' => $this->reportWriterMock,
+ 'cryptographer' => $this->cryptographerMock,
+ 'fileRecorder' => $this->fileRecorderMock,
+ 'subdirectoryPath' => $this->subdirectoryPath,
+ 'archiveName' => $this->archiveName,
+ ]
+ );
+ }
+
+ /**
+ * @param bool $isArchiveSourceDirectory
+ * @dataProvider prepareExportDataDataProvider
+ */
+ public function testPrepareExportData($isArchiveSourceDirectory)
+ {
+ $tmpFilesDirectoryPath = $this->subdirectoryPath . 'tmp/';
+ $archiveRelativePath = $this->subdirectoryPath . $this->archiveName;
+
+ $archiveSource = $isArchiveSourceDirectory ? (__DIR__) : '/tmp/' . $tmpFilesDirectoryPath;
+ $archiveAbsolutePath = '/tmp/' . $archiveRelativePath;
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::SYS_TMP)
+ ->willReturn($this->directoryMock);
+ $this->directoryMock
+ ->expects($this->exactly(4))
+ ->method('delete')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath]
+ );
+
+ $this->directoryMock
+ ->expects($this->exactly(4))
+ ->method('getAbsolutePath')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath],
+ [$archiveRelativePath]
+ )
+ ->willReturnOnConsecutiveCalls(
+ $archiveSource,
+ $archiveSource,
+ $archiveAbsolutePath,
+ $archiveAbsolutePath
+ );
+
+ $this->reportWriterMock
+ ->expects($this->once())
+ ->method('write')
+ ->with($this->directoryMock, $tmpFilesDirectoryPath);
+
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('isExist')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$archiveRelativePath]
+ )
+ ->willReturnOnConsecutiveCalls(
+ true,
+ true
+ );
+
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(dirname($archiveRelativePath));
+
+ $this->archiveMock
+ ->expects($this->once())
+ ->method('pack')
+ ->with(
+ $archiveSource,
+ $archiveAbsolutePath,
+ $isArchiveSourceDirectory ? true : false
+ );
+
+ $fileContent = 'Some text';
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('readFile')
+ ->with($archiveRelativePath)
+ ->willReturn($fileContent);
+
+ $this->cryptographerMock
+ ->expects($this->once())
+ ->method('encode')
+ ->with($fileContent)
+ ->willReturn($this->encodedContextMock);
+
+ $this->fileRecorderMock
+ ->expects($this->once())
+ ->method('recordNewFile')
+ ->with($this->encodedContextMock);
+
+ $this->assertTrue($this->exportDataHandler->prepareExportData());
+ }
+
+ /**
+ * @return array
+ */
+ public function prepareExportDataDataProvider()
+ {
+ return [
+ 'Data source for archive is directory' => [true],
+ 'Data source for archive doesn\'t directory' => [false],
+ ];
+ }
+
+ /**
+ * @return void
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testPrepareExportDataWithLocalizedException()
+ {
+ $tmpFilesDirectoryPath = $this->subdirectoryPath . 'tmp/';
+ $archivePath = $this->subdirectoryPath . $this->archiveName;
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::SYS_TMP)
+ ->willReturn($this->directoryMock);
+ $this->reportWriterMock
+ ->expects($this->once())
+ ->method('write')
+ ->with($this->directoryMock, $tmpFilesDirectoryPath);
+ $this->directoryMock
+ ->expects($this->exactly(3))
+ ->method('delete')
+ ->withConsecutive(
+ [$tmpFilesDirectoryPath],
+ [$tmpFilesDirectoryPath],
+ [$archivePath]
+ );
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('getAbsolutePath')
+ ->with($tmpFilesDirectoryPath);
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('isExist')
+ ->with($tmpFilesDirectoryPath)
+ ->willReturn(false);
+
+ $this->assertNull($this->exportDataHandler->prepareExportData());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php
new file mode 100644
index 0000000000000..da5f6af3ca4e1
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoManagerTest.php
@@ -0,0 +1,194 @@
+flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoFactoryMock = $this->getMockBuilder(FileInfoFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->fileInfoManager = $this->objectManagerHelper->getObject(
+ FileInfoManager::class,
+ [
+ 'flagManager' => $this->flagManagerMock,
+ 'fileInfoFactory' => $this->fileInfoFactoryMock,
+ 'flagCode' => $this->flagCode,
+ 'encodedParameters' => $this->encodedParameters,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testSave()
+ {
+ $path = 'path/to/file';
+ $initializationVector = openssl_random_pseudo_bytes(16);
+ $parameters = [
+ 'path' => $path,
+ 'initializationVector' => $initializationVector,
+ ];
+
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn($path);
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn($initializationVector);
+
+ foreach ($this->encodedParameters as $encodedParameter) {
+ $parameters[$encodedParameter] = base64_encode($parameters[$encodedParameter]);
+ }
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('saveFlag')
+ ->with($this->flagCode, $parameters);
+
+ $this->assertTrue($this->fileInfoManager->save($this->fileInfoMock));
+ }
+
+ /**
+ * @param string|null $path
+ * @param string|null $initializationVector
+ * @dataProvider saveWithLocalizedExceptionDataProvider
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testSaveWithLocalizedException($path, $initializationVector)
+ {
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getPath')
+ ->with()
+ ->willReturn($path);
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn($initializationVector);
+
+ $this->fileInfoManager->save($this->fileInfoMock);
+ }
+
+ /**
+ * @return array
+ */
+ public function saveWithLocalizedExceptionDataProvider()
+ {
+ return [
+ 'Empty FileInfo' => [null, null],
+ 'FileInfo without IV' => ['path/to/file', null],
+ ];
+ }
+
+ /**
+ * @dataProvider loadDataProvider
+ * @param array|null $parameters
+ */
+ public function testLoad($parameters)
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with($this->flagCode)
+ ->willReturn($parameters);
+
+ $processedParameters = $parameters ?: [];
+ $encodedParameters = array_intersect($this->encodedParameters, array_keys($processedParameters));
+ foreach ($encodedParameters as $encodedParameter) {
+ $processedParameters[$encodedParameter] = base64_decode($processedParameters[$encodedParameter]);
+ }
+
+ $this->fileInfoFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($processedParameters)
+ ->willReturn($this->fileInfoMock);
+
+ $this->assertSame($this->fileInfoMock, $this->fileInfoManager->load());
+ }
+
+ /**
+ * @return array
+ */
+ public function loadDataProvider()
+ {
+ return [
+ 'Empty flag data' => [null],
+ 'Correct flag data' => [[
+ 'path' => 'path/to/file',
+ 'initializationVector' => 'xUJjl54MVke+FvMFSBpRSA==',
+ ]],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php
new file mode 100644
index 0000000000000..43ce833f1f03f
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileInfoTest.php
@@ -0,0 +1,62 @@
+objectManagerHelper = new ObjectManagerHelper($this);
+ }
+
+ /**
+ * @param string|null $path
+ * @param string|null $initializationVector
+ * @return void
+ * @dataProvider constructDataProvider
+ */
+ public function testConstruct($path, $initializationVector)
+ {
+ $constructorArguments = [
+ 'path' => $path,
+ 'initializationVector' => $initializationVector,
+ ];
+ /** @var FileInfo $fileInfo */
+ $fileInfo = $this->objectManagerHelper->getObject(
+ FileInfo::class,
+ array_filter($constructorArguments)
+ );
+
+ $this->assertSame($path ?: '', $fileInfo->getPath());
+ $this->assertSame($initializationVector ?: '', $fileInfo->getInitializationVector());
+ }
+
+ /**
+ * @return array
+ */
+ public function constructDataProvider()
+ {
+ return [
+ 'Degenerate object' => [null, null],
+ 'Without Initialization Vector' => ['content text', null],
+ 'With Initialization Vector' => ['content text', 'c51sd3c4sd68c5sd'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php
new file mode 100644
index 0000000000000..3c9520bdd995b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/FileRecorderTest.php
@@ -0,0 +1,209 @@
+fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoFactoryMock = $this->getMockBuilder(FileInfoFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->filesystemMock = $this->getMockBuilder(Filesystem::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->encodedContextMock = $this->getMockBuilder(EncodedContext::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->fileRecorder = $this->objectManagerHelper->getObject(
+ FileRecorder::class,
+ [
+ 'fileInfoManager' => $this->fileInfoManagerMock,
+ 'fileInfoFactory' => $this->fileInfoFactoryMock,
+ 'filesystem' => $this->filesystemMock,
+ 'fileSubdirectoryPath' => $this->fileSubdirectoryPath,
+ 'encodedFileName' => $this->encodedFileName,
+ ]
+ );
+ }
+
+ /**
+ * @param string $pathToExistingFile
+ * @dataProvider recordNewFileDataProvider
+ */
+ public function testRecordNewFile($pathToExistingFile)
+ {
+ $content = openssl_random_pseudo_bytes(200);
+
+ $this->filesystemMock
+ ->expects($this->once())
+ ->method('getDirectoryWrite')
+ ->with(DirectoryList::MEDIA)
+ ->willReturn($this->directoryMock);
+
+ $this->encodedContextMock
+ ->expects($this->once())
+ ->method('getContent')
+ ->with()
+ ->willReturn($content);
+
+ $hashLength = 64;
+ $fileRelativePathPattern = '#' . preg_quote($this->fileSubdirectoryPath, '#')
+ . '.{' . $hashLength . '}/' . preg_quote($this->encodedFileName, '#') . '#';
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('writeFile')
+ ->with($this->matchesRegularExpression($fileRelativePathPattern), $content)
+ ->willReturn($this->directoryMock);
+
+ $this->fileInfoManagerMock
+ ->expects($this->once())
+ ->method('load')
+ ->with()
+ ->willReturn($this->fileInfoMock);
+
+ $this->encodedContextMock
+ ->expects($this->once())
+ ->method('getInitializationVector')
+ ->with()
+ ->willReturn('init_vector***');
+
+ /** register file */
+ $this->fileInfoFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->callback(
+ function ($parameters) {
+ return !empty($parameters['path']) && ('init_vector***' === $parameters['initializationVector']);
+ }
+ ))
+ ->willReturn($this->fileInfoMock);
+ $this->fileInfoManagerMock
+ ->expects($this->once())
+ ->method('save')
+ ->with($this->fileInfoMock);
+
+ /** remove old file */
+ $this->fileInfoMock
+ ->expects($this->exactly($pathToExistingFile ? 3 : 1))
+ ->method('getPath')
+ ->with()
+ ->willReturn($pathToExistingFile);
+ $directoryName = dirname($pathToExistingFile);
+ if ($directoryName === '.') {
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('delete')
+ ->with($pathToExistingFile);
+ } elseif ($directoryName) {
+ $this->directoryMock
+ ->expects($this->exactly(2))
+ ->method('delete')
+ ->withConsecutive(
+ [$pathToExistingFile],
+ [$directoryName]
+ );
+ }
+
+ $this->assertTrue($this->fileRecorder->recordNewFile($this->encodedContextMock));
+ }
+
+ /**
+ * @return array
+ */
+ public function recordNewFileDataProvider()
+ {
+ return [
+ 'File doesn\'t exist' => [''],
+ 'Existing file into subdirectory' => ['dir_name/file.txt'],
+ 'Existing file doesn\'t into subdirectory' => ['file.txt'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php
new file mode 100644
index 0000000000000..3076a22c85be4
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/IntegrationManagerTest.php
@@ -0,0 +1,228 @@
+integrationServiceMock = $this->getMockBuilder(IntegrationServiceInterface::class)
+ ->getMock();
+ $this->configMock = $this->getMockBuilder(Config::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->oauthServiceMock = $this->getMockBuilder(OauthServiceInterface::class)
+ ->getMock();
+ $this->integrationMock = $this->getMockBuilder(Integration::class)
+ ->disableOriginalConstructor()
+ ->setMethods([
+ 'getId',
+ 'getConsumerId'
+ ])
+ ->getMock();
+ $this->integrationManager = $objectManagerHelper->getObject(
+ IntegrationManager::class,
+ [
+ 'integrationService' => $this->integrationServiceMock,
+ 'oauthService' => $this->oauthServiceMock,
+ 'config' => $this->configMock
+ ]
+ );
+ }
+
+ /**
+ * @param string $status
+ *
+ * @return array
+ */
+ private function getIntegrationUserData($status)
+ {
+ return [
+ 'name' => 'ma-integration-user',
+ 'status' => $status,
+ 'all_resources' => false,
+ 'resource' => [
+ 'Magento_Analytics::analytics',
+ 'Magento_Analytics::analytics_api'
+ ],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testActivateIntegrationSuccess()
+ {
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->exactly(2))
+ ->method('getId')
+ ->willReturn(100500);
+ $integrationData = $this->getIntegrationUserData(Integration::STATUS_ACTIVE);
+ $integrationData['integration_id'] = 100500;
+ $this->configMock->expects($this->exactly(2))
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('update')
+ ->with($integrationData);
+ $this->assertTrue($this->integrationManager->activateIntegration());
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\NoSuchEntityException
+ */
+ public function testActivateIntegrationFailureNoSuchEntity()
+ {
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(null);
+ $this->configMock->expects($this->once())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->never())
+ ->method('update');
+ $this->integrationManager->activateIntegration();
+ }
+
+ /**
+ * @dataProvider integrationIdDataProvider
+ *
+ * @param int|null $integrationId If null integration is absent.
+ * @return void
+ */
+ public function testGetTokenNewIntegration($integrationId)
+ {
+ $this->configMock->expects($this->atLeastOnce())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getConsumerId')
+ ->willReturn(100500);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($integrationId);
+ if (!$integrationId) {
+ $this->integrationServiceMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->getIntegrationUserData(Integration::STATUS_INACTIVE))
+ ->willReturn($this->integrationMock);
+ }
+ $this->oauthServiceMock->expects($this->at(0))
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn(false);
+ $this->oauthServiceMock->expects($this->at(2))
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn('IntegrationToken');
+ $this->oauthServiceMock->expects($this->once())
+ ->method('createAccessToken')
+ ->with(100500, true)
+ ->willReturn(true);
+ $this->assertEquals('IntegrationToken', $this->integrationManager->generateToken());
+ }
+
+ /**
+ * @dataProvider integrationIdDataProvider
+ *
+ * @param int|null $integrationId If null integration is absent.
+ * @return void
+ */
+ public function testGetTokenExistingIntegration($integrationId)
+ {
+ $this->configMock->expects($this->atLeastOnce())
+ ->method('getConfigDataValue')
+ ->with('analytics/integration_name', null, null)
+ ->willReturn('ma-integration-user');
+ $this->integrationServiceMock->expects($this->once())
+ ->method('findByName')
+ ->with('ma-integration-user')
+ ->willReturn($this->integrationMock);
+ $this->integrationMock->expects($this->once())
+ ->method('getConsumerId')
+ ->willReturn(100500);
+ $this->integrationMock->expects($this->once())
+ ->method('getId')
+ ->willReturn($integrationId);
+ if (!$integrationId) {
+ $this->integrationServiceMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->getIntegrationUserData(Integration::STATUS_INACTIVE))
+ ->willReturn($this->integrationMock);
+ }
+ $this->oauthServiceMock->expects($this->once())
+ ->method('getAccessToken')
+ ->with(100500)
+ ->willReturn('IntegrationToken');
+ $this->oauthServiceMock->expects($this->never())
+ ->method('createAccessToken');
+ $this->assertEquals('IntegrationToken', $this->integrationManager->generateToken());
+ }
+
+ /**
+ * @return array
+ */
+ public function integrationIdDataProvider()
+ {
+ return [
+ [1],
+ [null],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php
new file mode 100644
index 0000000000000..c7aa2219d1eee
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php
@@ -0,0 +1,166 @@
+linkInterfaceFactoryMock = $this->getMockBuilder(LinkInterfaceFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+ $this->fileInfoManagerMock = $this->getMockBuilder(FileInfoManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeManagerInterfaceMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->getMockForAbstractClass();
+ $this->linkInterfaceMock = $this->getMockBuilder(LinkInterface::class)
+ ->getMockForAbstractClass();
+ $this->fileInfoMock = $this->getMockBuilder(FileInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeMock = $this->getMockBuilder(Store::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->linkProvider = $this->objectManagerHelper->getObject(
+ LinkProvider::class,
+ [
+ 'linkFactory' => $this->linkInterfaceFactoryMock,
+ 'fileInfoManager' => $this->fileInfoManagerMock,
+ 'storeManager' => $this->storeManagerInterfaceMock
+ ]
+ );
+ }
+
+ public function testGet()
+ {
+ $baseUrl = 'http://magento.local/pub/media/';
+ $fileInfoPath = 'analytics/data.tgz';
+ $fileInitializationVector = 'er312esq23eqq';
+ $this->fileInfoManagerMock->expects($this->once())
+ ->method('load')
+ ->willReturn($this->fileInfoMock);
+ $this->linkInterfaceFactoryMock->expects($this->once())
+ ->method('create')
+ ->with(
+ [
+ 'initializationVector' => base64_encode($fileInitializationVector),
+ 'url' => $baseUrl . $fileInfoPath
+ ]
+ )
+ ->willReturn($this->linkInterfaceMock);
+ $this->storeManagerInterfaceMock->expects($this->once())
+ ->method('getStore')->willReturn($this->storeMock);
+ $this->storeMock->expects($this->once())
+ ->method('getBaseUrl')
+ ->with(
+ UrlInterface::URL_TYPE_MEDIA
+ )
+ ->willReturn($baseUrl);
+ $this->fileInfoMock->expects($this->atLeastOnce())
+ ->method('getPath')
+ ->willReturn($fileInfoPath);
+ $this->fileInfoMock->expects($this->atLeastOnce())
+ ->method('getInitializationVector')
+ ->willReturn($fileInitializationVector);
+ $this->assertEquals($this->linkInterfaceMock, $this->linkProvider->get());
+ }
+
+ /**
+ * @param string|null $fileInfoPath
+ * @param string|null $fileInitializationVector
+ *
+ * @dataProvider fileNotReadyDataProvider
+ * @expectedException \Magento\Framework\Exception\NoSuchEntityException
+ * @expectedExceptionMessage File is not ready yet.
+ */
+ public function testFileNotReady($fileInfoPath, $fileInitializationVector)
+ {
+ $this->fileInfoManagerMock->expects($this->once())
+ ->method('load')
+ ->willReturn($this->fileInfoMock);
+ $this->fileInfoMock->expects($this->once())
+ ->method('getPath')
+ ->willReturn($fileInfoPath);
+ $this->fileInfoMock->expects($this->any())
+ ->method('getInitializationVector')
+ ->willReturn($fileInitializationVector);
+ $this->linkProvider->get();
+ }
+
+ /**
+ * @return array
+ */
+ public function fileNotReadyDataProvider()
+ {
+ return [
+ [null, 'initVector'],
+ ['path', null],
+ ['', 'initVector'],
+ ['path', ''],
+ ['', ''],
+ [null, null]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php
new file mode 100644
index 0000000000000..a89e06562383b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/Plugin/BaseUrlConfigPluginTest.php
@@ -0,0 +1,147 @@
+subscriptionUpdateHandlerMock = $this->getMockBuilder(SubscriptionUpdateHandler::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->configValueMock = $this->getMockBuilder(Value::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isValueChanged', 'getPath', 'getScope', 'getOldValue'])
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->plugin = $this->objectManagerHelper->getObject(
+ BaseUrlConfigPlugin::class,
+ [
+ 'subscriptionUpdateHandler' => $this->subscriptionUpdateHandlerMock,
+ ]
+ );
+ }
+
+ /**
+ * @param array $configValueData
+ * @return void
+ * @dataProvider afterSavePluginIsNotApplicableDataProvider
+ */
+ public function testAfterSavePluginIsNotApplicable(
+ array $configValueData
+ ) {
+ $this->configValueMock
+ ->method('isValueChanged')
+ ->willReturn($configValueData['isValueChanged']);
+ $this->configValueMock
+ ->method('getPath')
+ ->willReturn($configValueData['path']);
+ $this->configValueMock
+ ->method('getScope')
+ ->willReturn($configValueData['scope']);
+ $this->subscriptionUpdateHandlerMock
+ ->expects($this->never())
+ ->method('processUrlUpdate');
+
+ $this->assertEquals(
+ $this->configValueMock,
+ $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function afterSavePluginIsNotApplicableDataProvider()
+ {
+ return [
+ 'Value has not been changed' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => false,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL,
+ 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+ ],
+ ],
+ 'Unsecure URL has been changed' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => true,
+ 'path' => Store::XML_PATH_UNSECURE_BASE_URL,
+ 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+ ],
+ ],
+ 'Secure URL has been changed not in the Default scope' => [
+ 'Config Value Data' => [
+ 'isValueChanged' => true,
+ 'path' => Store::XML_PATH_SECURE_BASE_URL,
+ 'scope' => ScopeInterface::SCOPE_STORES
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testAfterSavePluginIsApplicable()
+ {
+ $this->configValueMock
+ ->method('isValueChanged')
+ ->willReturn(true);
+ $this->configValueMock
+ ->method('getPath')
+ ->willReturn(Store::XML_PATH_SECURE_BASE_URL);
+ $this->configValueMock
+ ->method('getScope')
+ ->willReturn(ScopeConfigInterface::SCOPE_TYPE_DEFAULT);
+ $this->configValueMock
+ ->method('getOldValue')
+ ->willReturn('http://store.com');
+ $this->subscriptionUpdateHandlerMock
+ ->expects($this->once())
+ ->method('processUrlUpdate')
+ ->with('http://store.com');
+
+ $this->assertEquals(
+ $this->configValueMock,
+ $this->plugin->afterAfterSave($this->configValueMock, $this->configValueMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php
new file mode 100644
index 0000000000000..0607a977e5b68
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php
@@ -0,0 +1,149 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->otpRequestMock = $this->getMockBuilder(OTPRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportUrlProvider = $this->objectManagerHelper->getObject(
+ ReportUrlProvider::class,
+ [
+ 'config' => $this->configMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'otpRequest' => $this->otpRequestMock,
+ 'flagManager' => $this->flagManagerMock,
+ 'urlReportConfigPath' => $this->urlReportConfigPath,
+ ]
+ );
+ }
+
+ /**
+ * @param bool $isTokenExist
+ * @param string|null $otp If null OTP was not received.
+ *
+ * @dataProvider getUrlDataProvider
+ */
+ public function testGetUrl($isTokenExist, $otp)
+ {
+ $reportUrl = 'https://example.com/report';
+ $url = '';
+
+ $this->configMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with($this->urlReportConfigPath)
+ ->willReturn($reportUrl);
+ $this->analyticsTokenMock
+ ->expects($this->once())
+ ->method('isTokenExist')
+ ->with()
+ ->willReturn($isTokenExist);
+ $this->otpRequestMock
+ ->expects($isTokenExist ? $this->once() : $this->never())
+ ->method('call')
+ ->with()
+ ->willReturn($otp);
+ if ($isTokenExist && $otp) {
+ $url = $reportUrl . '?' . http_build_query(['otp' => $otp], '', '&');
+ }
+ $this->assertSame($url ?: $reportUrl, $this->reportUrlProvider->getUrl());
+ }
+
+ /**
+ * @return array
+ */
+ public function getUrlDataProvider()
+ {
+ return [
+ 'TokenDoesNotExist' => [false, null],
+ 'TokenExistAndOtpEmpty' => [true, null],
+ 'TokenExistAndOtpValid' => [true, '249e6b658877bde2a77bc4ab'],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetUrlWhenSubscriptionUpdateRunning()
+ {
+ $this->flagManagerMock
+ ->expects($this->once())
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn('http://store.com');
+ $this->expectException(SubscriptionUpdateException::class);
+ $this->reportUrlProvider->getUrl();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php
new file mode 100644
index 0000000000000..d9b030b84d639
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php
@@ -0,0 +1,213 @@
+configInterfaceMock = $this->getMockBuilder(ConfigInterface::class)->getMockForAbstractClass();
+ $this->reportValidatorMock = $this->getMockBuilder(ReportValidator::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->providerFactoryMock = $this->getMockBuilder(ProviderFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->reportProviderMock = $this->getMockBuilder(ReportProvider::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->directoryMock = $this->getMockBuilder(WriteInterface::class)->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportWriter = $this->objectManagerHelper->getObject(
+ ReportWriter::class,
+ [
+ 'config' => $this->configInterfaceMock,
+ 'reportValidator' => $this->reportValidatorMock,
+ 'providerFactory' => $this->providerFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @param array $configData
+ * @return void
+ *
+ * @dataProvider configDataProvider
+ */
+ public function testWrite(array $configData)
+ {
+ $errors = [];
+ $fileData = [
+ ['number' => 1, 'type' => 'Shoes Usual']
+ ];
+ $this->configInterfaceMock
+ ->expects($this->once())
+ ->method('get')
+ ->with()
+ ->willReturn([$configData]);
+ $this->providerFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->with($this->providerClass)
+ ->willReturn($this->reportProviderMock);
+ $parameterName = isset(reset($configData)[0]['parameters']['name'])
+ ? reset($configData)[0]['parameters']['name']
+ : '';
+ $this->reportProviderMock->expects($this->once())
+ ->method('getReport')
+ ->with($parameterName ?: null)
+ ->willReturn($fileData);
+ $errorStreamMock = $this->getMockBuilder(
+ \Magento\Framework\Filesystem\File\WriteInterface::class
+ )->getMockForAbstractClass();
+ $errorStreamMock
+ ->expects($this->once())
+ ->method('lock')
+ ->with();
+ $errorStreamMock
+ ->expects($this->exactly(2))
+ ->method('writeCsv')
+ ->withConsecutive(
+ [array_keys($fileData[0])],
+ [$fileData[0]]
+ );
+ $errorStreamMock->expects($this->once())->method('unlock');
+ $errorStreamMock->expects($this->once())->method('close');
+ if ($parameterName) {
+ $this->reportValidatorMock
+ ->expects($this->once())
+ ->method('validate')
+ ->with($parameterName)
+ ->willReturn($errors);
+ }
+ $this->directoryMock
+ ->expects($this->once())
+ ->method('openFile')
+ ->with(
+ $this->stringContains('/var/tmp' . $parameterName ?: $this->reportName),
+ 'w+'
+ )->willReturn($errorStreamMock);
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @param array $configData
+ * @return void
+ *
+ * @dataProvider configDataProvider
+ */
+ public function testWriteErrorFile($configData)
+ {
+ $errors = ['orders', 'SQL Error: test'];
+ $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([$configData]);
+ $errorStreamMock = $this->getMockBuilder(
+ \Magento\Framework\Filesystem\File\WriteInterface::class
+ )->getMockForAbstractClass();
+ $errorStreamMock->expects($this->once())->method('lock');
+ $errorStreamMock->expects($this->once())->method('writeCsv')->with($errors);
+ $errorStreamMock->expects($this->once())->method('unlock');
+ $errorStreamMock->expects($this->once())->method('close');
+ $this->reportValidatorMock->expects($this->once())->method('validate')->willReturn($errors);
+ $this->directoryMock->expects($this->once())->method('openFile')->with('/var/tmp' . 'errors.csv', 'w+')
+ ->willReturn($errorStreamMock);
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testWriteEmptyReports()
+ {
+ $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([]);
+ $this->reportValidatorMock->expects($this->never())->method('validate');
+ $this->directoryMock->expects($this->never())->method('openFile');
+ $this->assertTrue($this->reportWriter->write($this->directoryMock, '/var/tmp'));
+ }
+
+ /**
+ * @return array
+ */
+ public function configDataProvider()
+ {
+ return [
+ 'reportProvider' => [
+ [
+ 'providers' => [
+ [
+ 'name' => $this->providerName,
+ 'class' => $this->providerClass,
+ 'parameters' => [
+ 'name' => $this->reportName
+ ],
+ ]
+ ]
+ ]
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php
new file mode 100644
index 0000000000000..f314d77f32b41
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportXml/ModuleIteratorTest.php
@@ -0,0 +1,50 @@
+moduleManagerMock = $this->getMockBuilder(ModuleManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $objectManagerHelper = new ObjectManagerHelper($this);
+ $this->moduleIterator = $objectManagerHelper->getObject(
+ ModuleIterator::class,
+ [
+ 'moduleManager' => $this->moduleManagerMock,
+ 'iterator' => new \ArrayIterator([0 => ['module_name' => 'Coco_Module']])
+ ]
+ );
+ }
+
+ public function testCurrent()
+ {
+ $this->moduleManagerMock->expects($this->once())
+ ->method('isEnabled')
+ ->with('Coco_Module')
+ ->willReturn(true);
+ foreach ($this->moduleIterator as $item) {
+ $this->assertEquals(['module_name' => 'Coco_Module', 'status' => 'Enabled'], $item);
+ }
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php
new file mode 100644
index 0000000000000..cc46d175543ad
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/StoreConfigurationProviderTest.php
@@ -0,0 +1,123 @@
+scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->websiteMock = $this->getMockBuilder(WebsiteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->storeMock = $this->getMockBuilder(StoreInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configPaths = [
+ 'web/unsecure/base_url',
+ 'currency/options/base',
+ 'general/locale/timezone'
+ ];
+
+ $this->storeConfigurationProvider = new StoreConfigurationProvider(
+ $this->scopeConfigMock,
+ $this->storeManagerMock,
+ $this->configPaths
+ );
+ }
+
+ public function testGetReport()
+ {
+ $map = [
+ ['web/unsecure/base_url', 'default', 0, '127.0.0.1'],
+ ['currency/options/base', 'default', 0, 'USD'],
+ ['general/locale/timezone', 'default', 0, 'America/Dawson'],
+ ['web/unsecure/base_url', 'websites', 1, '127.0.0.2'],
+ ['currency/options/base', 'websites', 1, 'USD'],
+ ['general/locale/timezone', 'websites', 1, 'America/Belem'],
+ ['web/unsecure/base_url', 'stores', 2, '127.0.0.3'],
+ ['currency/options/base', 'stores', 2, 'USD'],
+ ['general/locale/timezone', 'stores', 2, 'America/Phoenix'],
+ ];
+
+ $this->scopeConfigMock
+ ->method('getValue')
+ ->will($this->returnValueMap($map));
+
+ $this->storeManagerMock->expects($this->once())
+ ->method('getWebsites')
+ ->willReturn([$this->websiteMock]);
+
+ $this->storeManagerMock->expects($this->once())
+ ->method('getStores')
+ ->willReturn([$this->storeMock]);
+
+ $this->websiteMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(1);
+
+ $this->storeMock->expects($this->once())
+ ->method('getId')
+ ->willReturn(2);
+ $result = iterator_to_array($this->storeConfigurationProvider->getReport());
+ $resultValues = [];
+ foreach ($result as $item) {
+ $resultValues[] = array_values($item);
+ }
+ array_multisort($resultValues);
+ array_multisort($map);
+ $this->assertEquals($resultValues, $map);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php
new file mode 100644
index 0000000000000..d6b041ce03178
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/SubscriptionStatusProviderTest.php
@@ -0,0 +1,196 @@
+scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->analyticsTokenMock = $this->getMockBuilder(AnalyticsToken::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->flagManagerMock = $this->getMockBuilder(FlagManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->statusProvider = $this->objectManagerHelper->getObject(
+ SubscriptionStatusProvider::class,
+ [
+ 'scopeConfig' => $this->scopeConfigMock,
+ 'analyticsToken' => $this->analyticsTokenMock,
+ 'flagManager' => $this->flagManagerMock,
+ ]
+ );
+ }
+
+ /**
+ * @param array $flagManagerData
+ * @dataProvider getStatusShouldBeFailedDataProvider
+ */
+ public function testGetStatusShouldBeFailed(array $flagManagerData)
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(false);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+
+ $this->expectFlagManagerReturn($flagManagerData);
+ $this->assertEquals(SubscriptionStatusProvider::FAILED, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @return array
+ */
+ public function getStatusShouldBeFailedDataProvider()
+ {
+ return [
+ 'Subscription update doesn\'t active' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ ],
+ 'Subscription update is active' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @param array $flagManagerData
+ * @param bool $isTokenExist
+ * @dataProvider getStatusShouldBePendingDataProvider
+ */
+ public function testGetStatusShouldBePending(array $flagManagerData, bool $isTokenExist)
+ {
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn($isTokenExist);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+
+ $this->expectFlagManagerReturn($flagManagerData);
+ $this->assertEquals(SubscriptionStatusProvider::PENDING, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @return array
+ */
+ public function getStatusShouldBePendingDataProvider()
+ {
+ return [
+ 'Subscription update doesn\'t active and the token does not exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, null],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45]
+ ],
+ 'isTokenExist' => false,
+ ],
+ 'Subscription update is active and the token does not exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, 45]
+ ],
+ 'isTokenExist' => false,
+ ],
+ 'Subscription update is active and token exist' => [
+ 'Flag Manager data mapping' => [
+ [SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE, 'http://store.com'],
+ [SubscriptionHandler::ATTEMPTS_REVERSE_COUNTER_FLAG_CODE, null]
+ ],
+ 'isTokenExist' => true,
+ ],
+ ];
+ }
+
+ public function testGetStatusShouldBeEnabled()
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE)
+ ->willReturn(null);
+ $this->analyticsTokenMock->expects($this->once())
+ ->method('isTokenExist')
+ ->willReturn(true);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(true);
+ $this->assertEquals(SubscriptionStatusProvider::ENABLED, $this->statusProvider->getStatus());
+ }
+
+ public function testGetStatusShouldBeDisabled()
+ {
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('analytics/subscription/enabled')
+ ->willReturn(false);
+ $this->assertEquals(SubscriptionStatusProvider::DISABLED, $this->statusProvider->getStatus());
+ }
+
+ /**
+ * @param array $mapping
+ */
+ private function expectFlagManagerReturn(array $mapping)
+ {
+ $this->flagManagerMock
+ ->method('getFlagData')
+ ->willReturnMap($mapping);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php
new file mode 100644
index 0000000000000..ad1d87488d751
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/Model/System/Message/NotificationAboutFailedSubscriptionTest.php
@@ -0,0 +1,106 @@
+subscriptionStatusMock = $this->getMockBuilder(SubscriptionStatusProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->urlBuilderMock = $this->getMockBuilder(UrlInterface::class)
+ ->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->notification = $this->objectManagerHelper->getObject(
+ NotificationAboutFailedSubscription::class,
+ [
+ 'subscriptionStatusProvider' => $this->subscriptionStatusMock,
+ 'urlBuilder' => $this->urlBuilderMock
+ ]
+ );
+ }
+
+ public function testIsDisplayedWhenMessageShouldBeDisplayed()
+ {
+ $this->subscriptionStatusMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn(
+ SubscriptionStatusProvider::FAILED
+ );
+ $this->assertTrue($this->notification->isDisplayed());
+ }
+
+ /**
+ * @dataProvider notDisplayedNotificationStatuses
+ *
+ * @param $status
+ */
+ public function testIsDisplayedWhenMessageShouldNotBeDisplayed($status)
+ {
+ $this->subscriptionStatusMock->expects($this->once())
+ ->method('getStatus')
+ ->willReturn($status);
+ $this->assertFalse($this->notification->isDisplayed());
+ }
+
+ public function testGetTextShouldBuildMessage()
+ {
+ $retryUrl = 'http://magento.dev/retryUrl';
+ $this->urlBuilderMock->expects($this->once())
+ ->method('getUrl')
+ ->with('analytics/subscription/retry')
+ ->willReturn($retryUrl);
+ $messageDetails = 'Failed to synchronize data to the Magento Business Intelligence service. ';
+ $messageDetails .= sprintf('Retry Synchronization ', $retryUrl);
+ $this->assertEquals($messageDetails, $this->notification->getText());
+ }
+
+ /**
+ * Provide statuses according to which message should not be displayed.
+ *
+ * @return array
+ */
+ public function notDisplayedNotificationStatuses()
+ {
+ return [
+ [SubscriptionStatusProvider::PENDING],
+ [SubscriptionStatusProvider::DISABLED],
+ [SubscriptionStatusProvider::ENABLED],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php
new file mode 100644
index 0000000000000..3f1ed9a5cf4c0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/Converter/XmlTest.php
@@ -0,0 +1,121 @@
+objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\Config\Converter\Xml::class
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testConvertNoElements()
+ {
+ $this->assertEmpty(
+ $this->subject->convert(new \DOMDocument())
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testConvert()
+ {
+ $dom = new \DOMDocument();
+
+ $expectedArray = [
+ 'config' => [
+ [
+ 'noNamespaceSchemaLocation' => 'urn:magento:module:Magento_Analytics:etc/reports.xsd',
+ 'report' => [
+ [
+ 'name' => 'test_report_1',
+ 'connection' => 'sales',
+ 'source' => [
+ [
+ 'name' => 'sales_order',
+ 'alias' => 'orders',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id',
+ 'alias' => 'identifier',
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'gt',
+ '_value' => '10'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'name' => 'test_report_2',
+ 'connection' => 'default',
+ 'source' => [
+ [
+ 'name' => 'customer_entity',
+ 'alias' => 'customers',
+ 'attribute' => [
+ [
+ 'name' => 'email'
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'dob',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $dom->loadXML(file_get_contents(__DIR__ . '/../_files/valid_reports.xml'));
+
+ $this->assertEquals($expectedArray, $this->subject->convert($dom));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php
new file mode 100644
index 0000000000000..85343b6b301d6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/MapperTest.php
@@ -0,0 +1,47 @@
+mapper = new Mapper();
+ }
+
+ public function testExecute()
+ {
+ $configData['config'][0]['report'] = [
+ [
+ 'source' => ['product'],
+ 'name' => 'Product',
+ ]
+ ];
+ $expectedResult = [
+ 'Product' => [
+ 'source' => 'product',
+ 'name' => 'Product',
+ ]
+ ];
+ $this->assertEquals($this->mapper->execute($configData), $expectedResult);
+ }
+
+ public function testExecuteWithoutReports()
+ {
+ $configData = [];
+ $this->assertEquals($this->mapper->execute($configData), []);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml
new file mode 100644
index 0000000000000..e04ee96163797
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/Config/_files/valid_reports.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php
new file mode 100644
index 0000000000000..cbc9aa129d874
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConfigTest.php
@@ -0,0 +1,64 @@
+dataMock = $this->getMockBuilder(DataInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->config = $this->objectManagerHelper->getObject(
+ Config::class,
+ [
+ 'data' => $this->dataMock,
+ ]
+ );
+ }
+
+ public function testGet()
+ {
+ $queryName = 'query string';
+ $queryResult = [ 'query' => 1 ];
+
+ $this->dataMock
+ ->expects($this->once())
+ ->method('get')
+ ->with($queryName)
+ ->willReturn($queryResult);
+
+ $this->assertSame($queryResult, $this->config->get($queryName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php
new file mode 100644
index 0000000000000..1e4ae9142c13d
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php
@@ -0,0 +1,106 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(MysqlPdoAdapter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionNewMock = $this->getMockBuilder(MysqlPdoAdapter::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->connectionFactory = $this->objectManagerHelper->getObject(
+ ConnectionFactory::class,
+ [
+ 'resourceConnection' => $this->resourceConnectionMock,
+ 'objectManager' => $this->objectManagerMock,
+ ]
+ );
+ }
+
+ public function testGetConnection()
+ {
+ $connectionName = 'read';
+
+ $this->resourceConnectionMock
+ ->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+
+ $this->connectionMock
+ ->expects($this->once())
+ ->method('getConfig')
+ ->with()
+ ->willReturn(['persistent' => 1]);
+
+ $this->objectManagerMock
+ ->expects($this->once())
+ ->method('create')
+ ->with(get_class($this->connectionMock), ['config' => ['use_buffered_query' => false]])
+ ->willReturn($this->connectionNewMock);
+
+ $this->assertSame($this->connectionNewMock, $this->connectionFactory->getConnection($connectionName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php
new file mode 100644
index 0000000000000..3b01105a8873b
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FilterAssemblerTest.php
@@ -0,0 +1,143 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getFilters')
+ ->willReturn([]);
+
+ $this->conditionResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ConditionResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler::class,
+ [
+ 'conditionResolver' => $this->conditionResolverMock,
+ 'nameResolver' => $this->nameResolverMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales'
+ ]
+ ];
+
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setFilters');
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleNotEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+
+ $this->nameResolverMock->expects($this->any())
+ ->method('getAlias')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['alias']);
+
+ $this->conditionResolverMock->expects($this->once())
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['filter'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn('(sales.entity_id IS NULL)');
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFilters')
+ ->with(['(sales.entity_id IS NULL)']);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php
new file mode 100644
index 0000000000000..575db94a7b7e1
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/FromAssemblerTest.php
@@ -0,0 +1,167 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn([]);
+
+ $this->columnsResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ColumnsResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\FromAssembler::class,
+ [
+ 'nameResolver' => $this->nameResolverMock,
+ 'columnsResolver' => $this->columnsResolverMock,
+ 'resourceConnection' => $this->resourceConnection,
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider assembleDataProvider
+ * @param array $queryConfig
+ * @param string $tableName
+ * @return void
+ */
+ public function testAssemble(array $queryConfig, $tableName)
+ {
+ $this->nameResolverMock->expects($this->any())
+ ->method('getAlias')
+ ->with($queryConfig['source'])
+ ->willReturn($queryConfig['source']['alias']);
+
+ $this->nameResolverMock->expects($this->once())
+ ->method('getName')
+ ->with($queryConfig['source'])
+ ->willReturn($queryConfig['source']['name']);
+
+ $this->resourceConnection
+ ->expects($this->once())
+ ->method('getTableName')
+ ->with($queryConfig['source']['name'])
+ ->willReturn($tableName);
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFrom')
+ ->with([$queryConfig['source']['alias'] => $tableName]);
+
+ $this->columnsResolverMock->expects($this->once())
+ ->method('getColumns')
+ ->with($this->selectBuilderMock, $queryConfig['source'])
+ ->willReturn(['entity_id' => 'sales.entity_id']);
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setColumns')
+ ->with(['entity_id' => 'sales.entity_id']);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfig)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function assembleDataProvider()
+ {
+ return [
+ 'Tables without prefixes' => [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id'
+ ]
+ ],
+ ],
+ ],
+ 'sales_order',
+ ],
+ 'Tables with prefixes' => [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'attribute' => [
+ [
+ 'name' => 'entity_id'
+ ]
+ ],
+ ],
+ ],
+ 'pref_sales_order',
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php
new file mode 100644
index 0000000000000..aaafd731552a0
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/Assembler/JoinAssemblerTest.php
@@ -0,0 +1,279 @@
+nameResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\NameResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getFilters')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getJoins')
+ ->willReturn([]);
+
+ $this->columnsResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ColumnsResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->conditionResolverMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\ConditionResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler::class,
+ [
+ 'conditionResolver' => $this->conditionResolverMock,
+ 'nameResolver' => $this->nameResolverMock,
+ 'columnsResolver' => $this->columnsResolverMock,
+ 'resourceConnection' => $this->resourceConnection,
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testAssembleEmpty()
+ {
+ $queryConfigMock = [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales'
+ ]
+ ];
+
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setColumns');
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setFilters');
+ $this->selectBuilderMock->expects($this->never())
+ ->method('setJoins');
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @param array $queryConfigMock
+ * @param array $joinsMock
+ * @param array $tablesMapping
+ * @return void
+ * @dataProvider assembleNotEmptyDataProvider
+ */
+ public function testAssembleNotEmpty(array $queryConfigMock, array $joinsMock, array $tablesMapping)
+ {
+ $filtersMock = [];
+
+ $this->nameResolverMock->expects($this->at(0))
+ ->method('getAlias')
+ ->with($queryConfigMock['source'])
+ ->willReturn($queryConfigMock['source']['alias']);
+ $this->nameResolverMock->expects($this->at(1))
+ ->method('getAlias')
+ ->with($queryConfigMock['source']['link-source'][0])
+ ->willReturn($queryConfigMock['source']['link-source'][0]['alias']);
+ $this->nameResolverMock->expects($this->once())
+ ->method('getName')
+ ->with($queryConfigMock['source']['link-source'][0])
+ ->willReturn($queryConfigMock['source']['link-source'][0]['name']);
+
+ $this->resourceConnection
+ ->expects($this->any())
+ ->method('getTableName')
+ ->willReturnOnConsecutiveCalls(...array_values($tablesMapping));
+
+ $this->conditionResolverMock->expects($this->at(0))
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['link-source'][0]['using'],
+ $queryConfigMock['source']['link-source'][0]['alias'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn('(billing.parent_id = `sales`.`entity_id`)');
+
+ if (isset($queryConfigMock['source']['link-source'][0]['filter'])) {
+ $filtersMock = ['(sales.entity_id IS NULL)'];
+
+ $this->conditionResolverMock->expects($this->at(1))
+ ->method('getFilter')
+ ->with(
+ $this->selectBuilderMock,
+ $queryConfigMock['source']['link-source'][0]['filter'],
+ $queryConfigMock['source']['link-source'][0]['alias'],
+ $queryConfigMock['source']['alias']
+ )
+ ->willReturn($filtersMock[0]);
+
+ $this->columnsResolverMock->expects($this->once())
+ ->method('getColumns')
+ ->with($this->selectBuilderMock, $queryConfigMock['source']['link-source'][0])
+ ->willReturn(
+ [
+ 'entity_id' => 'sales.entity_id',
+ 'billing_address_id' => 'billing.entity_id'
+ ]
+ );
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setColumns')
+ ->with(
+ [
+ 'entity_id' => 'sales.entity_id',
+ 'billing_address_id' => 'billing.entity_id'
+ ]
+ );
+ }
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setFilters')
+ ->with($filtersMock);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setJoins')
+ ->with($joinsMock);
+
+ $this->assertEquals(
+ $this->selectBuilderMock,
+ $this->subject->assemble($this->selectBuilderMock, $queryConfigMock)
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function assembleNotEmptyDataProvider()
+ {
+ return [
+ [
+ [
+ 'source' => [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ 'link-source' => [
+ [
+ 'name' => 'sales_order_address',
+ 'alias' => 'billing',
+ 'link-type' => 'left',
+ 'attribute' => [
+ [
+ 'alias' => 'billing_address_id',
+ 'name' => 'entity_id'
+ ]
+ ],
+ 'using' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'parent_id',
+ 'operator' => 'eq',
+ 'type' => 'identifier',
+ '_value' => 'entity_id'
+ ]
+ ]
+ ]
+ ],
+ 'filter' => [
+ [
+ 'glue' => 'and',
+ 'condition' => [
+ [
+ 'attribute' => 'entity_id',
+ 'operator' => 'null'
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'billing' => [
+ 'link-type' => 'left',
+ 'table' => [
+ 'billing' => 'pref_sales_order_address'
+ ],
+ 'condition' => '(billing.parent_id = `sales`.`entity_id`)'
+ ]
+ ],
+ ['sales_order_address' => 'pref_sales_order_address']
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php
new file mode 100644
index 0000000000000..bdbe3d1d22c22
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ColumnsResolverTest.php
@@ -0,0 +1,150 @@
+selectBuilderMock = $this->getMockBuilder(SelectBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $objectManager = new ObjectManagerHelper($this);
+ $this->columnsResolver = $objectManager->getObject(
+ ColumnsResolver::class,
+ [
+ 'nameResolver' => new NameResolver(),
+ 'resourceConnection' => $this->resourceConnectionMock
+ ]
+ );
+ }
+
+ public function testGetColumnsWithoutAttributes()
+ {
+ $this->assertEquals($this->columnsResolver->getColumns($this->selectBuilderMock, []), []);
+ }
+
+ /**
+ * @dataProvider getColumnsDataProvider
+ */
+ public function testGetColumnsWithFunction($expectedColumns, $expectedGroup, $entityConfig)
+ {
+ $this->resourceConnectionMock->expects($this->any())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->any())
+ ->method('quoteIdentifier')
+ ->with('cpe.name')
+ ->willReturn('`cpe`.`name`');
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getColumns')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getGroup')
+ ->willReturn([]);
+ $this->selectBuilderMock->expects($this->once())
+ ->method('setGroup')
+ ->with($expectedGroup);
+ $this->assertEquals(
+ $expectedColumns,
+ $this->columnsResolver->getColumns(
+ $this->selectBuilderMock,
+ $entityConfig
+ )
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function getColumnsDataProvider()
+ {
+ return [
+ 'COUNT( DISTINCT `cpe`.`name`) AS name' => [
+ 'expectedColumns' => [
+ 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)')
+ ],
+ 'expectedGroup' => [
+ 'name' => new ColumnValueExpression('COUNT( DISTINCT `cpe`.`name`)')
+ ],
+ 'entityConfig' =>
+ [
+ 'name' => 'catalog_product_entity',
+ 'alias' => 'cpe',
+ 'attribute' => [
+ [
+ 'name' => 'name',
+ 'function' => 'COUNT',
+ 'distinct' => true,
+ 'group' => true
+ ]
+ ],
+ ],
+ ],
+ 'AVG(`cpe`.`name`) AS avg_name' => [
+ 'expectedColumns' => [
+ 'avg_name' => new ColumnValueExpression('AVG(`cpe`.`name`)')
+ ],
+ 'expectedGroup' => [],
+ 'entityConfig' =>
+ [
+ 'name' => 'catalog_product_entity',
+ 'alias' => 'cpe',
+ 'attribute' => [
+ [
+ 'name' => 'name',
+ 'alias' => 'avg_name',
+ 'function' => 'AVG',
+ ]
+ ],
+ ],
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php
new file mode 100644
index 0000000000000..c8182d068fba5
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ConditionResolverTest.php
@@ -0,0 +1,108 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderMock = $this->getMockBuilder(SelectBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->conditionResolver = new ConditionResolver($this->resourceConnectionMock);
+ }
+
+ public function testGetFilter()
+ {
+ $condition = ["type" => "variable", "_value" => "1", "attribute" => "id", "operator" => "neq"];
+ $valueCondition = ["type" => "value", "_value" => "2", "attribute" => "first_name", "operator" => "eq"];
+ $identifierCondition = [
+ "type" => "identifier",
+ "_value" => "other_field",
+ "attribute" => "last_name",
+ "operator" => "eq"];
+ $filter = [["glue" => "AND", "condition" => [$valueCondition]]];
+ $filterConfig = [
+ ["glue" => "OR", "condition" => [$condition], 'filter' => $filter],
+ ["glue" => "OR", "condition" => [$identifierCondition]],
+ ];
+ $aliasName = 'n';
+ $this->selectBuilderMock->expects($this->any())
+ ->method('setParams')
+ ->with(array_merge([], [$condition['_value']]));
+
+ $this->selectBuilderMock->expects($this->once())
+ ->method('getParams')
+ ->willReturn([]);
+
+ $this->selectBuilderMock->expects($this->any())
+ ->method('getColumns')
+ ->willReturn(['price' => new Expression("(n.price = 400)")]);
+
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+
+ $this->connectionMock->expects($this->any())
+ ->method('quote')
+ ->willReturn("'John'");
+ $this->connectionMock->expects($this->exactly(4))
+ ->method('quoteIdentifier')
+ ->willReturnMap([
+ ['n.id', false, '`n`.`id`'],
+ ['n.first_name', false, '`n`.`first_name`'],
+ ['n.last_name', false, '`n`.`last_name`'],
+ ['other_field', false, '`other_field`'],
+ ]);
+
+ $result = "(`n`.`id` != 1 OR ((`n`.`first_name` = 'John'))) OR (`n`.`last_name` = `other_field`)";
+ $this->assertEquals(
+ $result,
+ $this->conditionResolver->getFilter($this->selectBuilderMock, $filterConfig, $aliasName)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php
new file mode 100644
index 0000000000000..4accd03aef3ea
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/NameResolverTest.php
@@ -0,0 +1,90 @@
+nameResolverMock = $this->getMockBuilder(NameResolver::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getName'])
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->nameResolver = $this->objectManagerHelper->getObject(NameResolver::class);
+ }
+
+ public function testGetName()
+ {
+ $elementConfigMock = [
+ 'name' => 'sales_order',
+ 'alias' => 'sales',
+ ];
+
+ $this->assertSame('sales_order', $this->nameResolver->getName($elementConfigMock));
+ }
+
+ /**
+ * @param array $elementConfig
+ * @param string|null $elementAlias
+ *
+ * @dataProvider getAliasDataProvider
+ */
+ public function testGetAlias($elementConfig, $elementAlias)
+ {
+ $elementName = 'elementName';
+
+ $this->nameResolverMock
+ ->expects($this->once())
+ ->method('getName')
+ ->with($elementConfig)
+ ->willReturn($elementName);
+
+ $this->assertSame($elementAlias ?: $elementName, $this->nameResolverMock->getAlias($elementConfig));
+ }
+
+ /**
+ * @return array
+ */
+ public function getAliasDataProvider()
+ {
+ return [
+ 'ElementConfigWithAliases' => [
+ ['alias' => 'sales', 'name' => 'sales_order'],
+ 'sales',
+ ],
+ 'ElementConfigWithoutAliases' => [
+ ['name' => 'sales_order'],
+ null,
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php
new file mode 100644
index 0000000000000..bbb9ca4b511b6
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/ReportValidatorTest.php
@@ -0,0 +1,125 @@
+connectionFactoryMock = $this->getMockBuilder(ConnectionFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->queryFactoryMock = $this->getMockBuilder(QueryFactory::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->queryMock = $this->getMockBuilder(Query::class)->disableOriginalConstructor()
+ ->getMock();
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass();
+ $this->selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->reportValidator = $this->objectManagerHelper->getObject(
+ ReportValidator::class,
+ [
+ 'connectionFactory' => $this->connectionFactoryMock,
+ 'queryFactory' => $this->queryFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider errorDataProvider
+ * @param string $reportName
+ * @param array $result
+ * @param \PHPUnit_Framework_MockObject_Stub $queryReturnStub
+ */
+ public function testValidate($reportName, $result, \PHPUnit_Framework_MockObject_Stub $queryReturnStub)
+ {
+ $connectionName = 'testConnection';
+ $this->queryFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($this->queryMock);
+ $this->queryMock->expects($this->once())->method('getConnectionName')->willReturn($connectionName);
+ $this->connectionFactoryMock->expects($this->once())->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+ $this->queryMock->expects($this->atLeastOnce())->method('getSelect')->willReturn($this->selectMock);
+ $this->selectMock->expects($this->once())->method('limit')->with(0);
+ $this->connectionMock->expects($this->once())->method('query')->with($this->selectMock)->will($queryReturnStub);
+ $this->assertEquals($result, $this->reportValidator->validate($reportName));
+ }
+
+ /**
+ * Provide variations of the error returning
+ *
+ * @return array
+ */
+ public function errorDataProvider()
+ {
+ $reportName = 'test';
+ $errorMessage = 'SQL Error 42';
+ return [
+ [
+ $reportName,
+ 'expectedResult' => [],
+ 'queryReturnStub' => $this->returnValue(null)
+ ],
+ [
+ $reportName,
+ 'expectedResult' => [$reportName, $errorMessage],
+ 'queryReturnStub' => $this->throwException(new \Zend_Db_Statement_Exception($errorMessage))
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php
new file mode 100644
index 0000000000000..a82a187cdb3f8
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php
@@ -0,0 +1,103 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilder = new SelectBuilder($this->resourceConnectionMock);
+ }
+
+ public function testCreate()
+ {
+ $connectionName = 'MySql';
+ $from = ['customer c'];
+ $columns = ['id', 'name', 'price'];
+ $filter = 'filter';
+ $joins = [
+ ['link-type' => 'left', 'table' => 'customer', 'condition' => 'in'],
+ ['link-type' => 'inner', 'table' => 'price', 'condition' => 'eq'],
+ ['link-type' => 'right', 'table' => 'attribute', 'condition' => 'neq'],
+ ];
+ $groups = ['id', 'name'];
+ $this->selectBuilder->setConnectionName($connectionName);
+ $this->selectBuilder->setFrom($from);
+ $this->selectBuilder->setColumns($columns);
+ $this->selectBuilder->setFilters([$filter]);
+ $this->selectBuilder->setJoins($joins);
+ $this->selectBuilder->setGroup($groups);
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('select')
+ ->willReturn($this->selectMock);
+ $this->selectMock->expects($this->once())
+ ->method('from')
+ ->with($from, []);
+ $this->selectMock->expects($this->once())
+ ->method('columns')
+ ->with($columns);
+ $this->selectMock->expects($this->once())
+ ->method('where')
+ ->with($filter);
+ $this->selectMock->expects($this->once())
+ ->method('joinLeft')
+ ->with($joins[0]['table'], $joins[0]['condition'], []);
+ $this->selectMock->expects($this->once())
+ ->method('joinInner')
+ ->with($joins[1]['table'], $joins[1]['condition'], []);
+ $this->selectMock->expects($this->once())
+ ->method('joinRight')
+ ->with($joins[2]['table'], $joins[2]['condition'], []);
+ $this->selectBuilder->create();
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php
new file mode 100644
index 0000000000000..1d3f293ed676a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/IteratorFactoryTest.php
@@ -0,0 +1,59 @@
+objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorIteratorMock = $this->getMockBuilder(\IteratorIterator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorFactory = new IteratorFactory(
+ $this->objectManagerMock
+ );
+ }
+
+ public function testCreate()
+ {
+ $arrayObject = new \ArrayIterator([1, 2, 3, 4, 5]);
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(\IteratorIterator::class, ['iterator' => $arrayObject])
+ ->willReturn($this->iteratorIteratorMock);
+
+ $this->assertEquals($this->iteratorFactory->create($arrayObject), $this->iteratorIteratorMock);
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php
new file mode 100644
index 0000000000000..9a3805a50f167
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryFactoryTest.php
@@ -0,0 +1,239 @@
+queryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Query::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Config::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Select::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->assemblerMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\Assembler\AssemblerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryCacheMock = $this->getMockBuilder(
+ \Magento\Framework\App\CacheInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(
+ \Magento\Framework\ObjectManagerInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectHydratorMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\SelectHydrator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectBuilderFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilderFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\QueryFactory::class,
+ [
+ 'config' => $this->configMock,
+ 'selectBuilderFactory' => $this->selectBuilderFactoryMock,
+ 'assemblers' => [$this->assemblerMock],
+ 'queryCache' => $this->queryCacheMock,
+ 'objectManager' => $this->objectManagerMock,
+ 'selectHydrator' => $this->selectHydratorMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreateCached()
+ {
+ $queryName = 'test_query';
+
+ $this->queryCacheMock->expects($this->any())
+ ->method('load')
+ ->with($queryName)
+ ->willReturn('{"connectionName":"sales","config":{},"select_parts":{}}');
+
+ $this->selectHydratorMock->expects($this->any())
+ ->method('recreate')
+ ->with([])
+ ->willReturn($this->selectMock);
+
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(
+ \Magento\Analytics\ReportXml\Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'connectionName' => 'sales',
+ 'config' => []
+ ]
+ )
+ ->willReturn($this->queryMock);
+
+ $this->queryCacheMock->expects($this->never())
+ ->method('save');
+
+ $this->assertEquals(
+ $this->queryMock,
+ $this->subject->create($queryName)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreateNotCached()
+ {
+ $queryName = 'test_query';
+
+ $queryConfigMock = [
+ 'name' => 'test_query',
+ 'connection' => 'sales'
+ ];
+
+ $selectBuilderMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\DB\SelectBuilder::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $selectBuilderMock->expects($this->once())
+ ->method('setConnectionName')
+ ->with($queryConfigMock['connection']);
+ $selectBuilderMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->selectMock);
+ $selectBuilderMock->expects($this->any())
+ ->method('getConnectionName')
+ ->willReturn($queryConfigMock['connection']);
+
+ $this->queryCacheMock->expects($this->any())
+ ->method('load')
+ ->with($queryName)
+ ->willReturn(null);
+
+ $this->configMock->expects($this->any())
+ ->method('get')
+ ->with($queryName)
+ ->willReturn($queryConfigMock);
+
+ $this->selectBuilderFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($selectBuilderMock);
+
+ $this->assemblerMock->expects($this->once())
+ ->method('assemble')
+ ->with($selectBuilderMock, $queryConfigMock)
+ ->willReturn($selectBuilderMock);
+
+ $this->objectManagerMock->expects($this->once())
+ ->method('create')
+ ->with(
+ \Magento\Analytics\ReportXml\Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'connectionName' => $queryConfigMock['connection'],
+ 'config' => $queryConfigMock
+ ]
+ )
+ ->willReturn($this->queryMock);
+
+ $this->queryCacheMock->expects($this->once())
+ ->method('save')
+ ->with(json_encode($this->queryMock), $queryName);
+
+ $this->assertEquals(
+ $this->queryMock,
+ $this->subject->create($queryName)
+ );
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php
new file mode 100644
index 0000000000000..a4b08a9ce5e0a
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/QueryTest.php
@@ -0,0 +1,90 @@
+selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectHydratorMock = $this->getMockBuilder(selectHydrator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->query = $this->objectManagerHelper->getObject(
+ Query::class,
+ [
+ 'select' => $this->selectMock,
+ 'connectionName' => $this->connectionName,
+ 'selectHydrator' => $this->selectHydratorMock,
+ 'config' => []
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testJsonSerialize()
+ {
+ $selectParts = ['part' => 1];
+
+ $this->selectHydratorMock
+ ->expects($this->once())
+ ->method('extract')
+ ->with($this->selectMock)
+ ->willReturn($selectParts);
+
+ $expectedResult = [
+ 'connectionName' => $this->connectionName,
+ 'select_parts' => $selectParts,
+ 'config' => []
+ ];
+
+ $this->assertSame($expectedResult, $this->query->jsonSerialize());
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php
new file mode 100644
index 0000000000000..5f329993dd291
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ReportProviderTest.php
@@ -0,0 +1,180 @@
+selectMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Select::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\Query::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->queryMock->expects($this->any())
+ ->method('getSelect')
+ ->willReturn($this->selectMock);
+
+ $this->iteratorMock = $this->getMockBuilder(
+ \IteratorIterator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->statementMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Statement\Pdo\Mysql::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->statementMock->expects($this->any())
+ ->method('getIterator')
+ ->willReturn($this->iteratorMock);
+
+ $this->connectionMock = $this->getMockBuilder(
+ \Magento\Framework\DB\Adapter\AdapterInterface::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->queryFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\QueryFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->iteratorFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\IteratorFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->iteratorMock = $this->getMockBuilder(
+ \IteratorIterator::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->objectManagerHelper =
+ new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+
+ $this->connectionFactoryMock = $this->getMockBuilder(
+ \Magento\Analytics\ReportXml\ConnectionFactory::class
+ )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->subject = $this->objectManagerHelper->getObject(
+ \Magento\Analytics\ReportXml\ReportProvider::class,
+ [
+ 'queryFactory' => $this->queryFactoryMock,
+ 'connectionFactory' => $this->connectionFactoryMock,
+ 'iteratorFactory' => $this->iteratorFactoryMock
+ ]
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetReport()
+ {
+ $reportName = 'test_report';
+ $connectionName = 'sales';
+
+ $this->queryFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($reportName)
+ ->willReturn($this->queryMock);
+
+ $this->connectionFactoryMock->expects($this->once())
+ ->method('getConnection')
+ ->with($connectionName)
+ ->willReturn($this->connectionMock);
+
+ $this->queryMock->expects($this->once())
+ ->method('getConnectionName')
+ ->willReturn($connectionName);
+
+ $this->queryMock->expects($this->once())
+ ->method('getConfig')
+ ->willReturn(
+ [
+ 'connection' => $connectionName
+ ]
+ );
+
+ $this->connectionMock->expects($this->once())
+ ->method('query')
+ ->with($this->selectMock)
+ ->willReturn($this->statementMock);
+
+ $this->iteratorFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($this->statementMock, null)
+ ->willReturn($this->iteratorMock);
+ $this->assertEquals($this->iteratorMock, $this->subject->getReport($reportName));
+ }
+}
diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php
new file mode 100644
index 0000000000000..ce57a1eca3689
--- /dev/null
+++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/SelectHydratorTest.php
@@ -0,0 +1,257 @@
+resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->selectMock = $this->getMockBuilder(Select::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->selectHydrator = $this->objectManagerHelper->getObject(
+ SelectHydrator::class,
+ [
+ 'resourceConnection' => $this->resourceConnectionMock,
+ 'objectManager' => $this->objectManagerMock,
+ ]
+ );
+ }
+
+ public function testExtract()
+ {
+ $selectParts =
+ [
+ Select::DISTINCT,
+ Select::COLUMNS,
+ Select::UNION,
+ Select::FROM,
+ Select::WHERE,
+ Select::GROUP,
+ Select::HAVING,
+ Select::ORDER,
+ Select::LIMIT_COUNT,
+ Select::LIMIT_OFFSET,
+ Select::FOR_UPDATE
+ ];
+
+ $result = [];
+ foreach ($selectParts as $part) {
+ $result[$part] = "Part";
+ }
+ $this->selectMock->expects($this->any())
+ ->method('getPart')
+ ->willReturn("Part");
+ $this->assertEquals($this->selectHydrator->extract($this->selectMock), $result);
+ }
+
+ /**
+ * @dataProvider recreateWithoutExpressionDataProvider
+ * @param array $selectParts
+ * @param array $parts
+ * @param array $partValues
+ */
+ public function testRecreateWithoutExpression($selectParts, $parts, $partValues)
+ {
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('select')
+ ->willReturn($this->selectMock);
+ foreach ($parts as $key => $part) {
+ $this->selectMock->expects($this->at($key))
+ ->method('setPart')
+ ->with($part, $partValues[$key]);
+ }
+
+ $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts));
+ }
+
+ /**
+ * @return array
+ */
+ public function recreateWithoutExpressionDataProvider()
+ {
+ return [
+ 'Select without expressions' => [
+ [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ 'field_name_2',
+ 'alias_2',
+ ],
+ ]
+ ],
+ [Select::COLUMNS],
+ [[
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ 'field_name_2',
+ 'alias_2',
+ ],
+ ]],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider recreateWithExpressionDataProvider
+ * @param array $selectParts
+ * @param array $expectedParts
+ * @param \PHPUnit_Framework_MockObject_MockObject[] $expressionMocks
+ */
+ public function testRecreateWithExpression(
+ array $selectParts,
+ array $expectedParts,
+ array $expressionMocks
+ ) {
+ $this->objectManagerMock
+ ->expects($this->exactly(count($expressionMocks)))
+ ->method('create')
+ ->with($this->isType('string'), $this->isType('array'))
+ ->willReturnOnConsecutiveCalls(...$expressionMocks);
+ $this->resourceConnectionMock
+ ->expects($this->once())
+ ->method('getConnection')
+ ->with()
+ ->willReturn($this->connectionMock);
+ $this->connectionMock
+ ->expects($this->once())
+ ->method('select')
+ ->with()
+ ->willReturn($this->selectMock);
+ foreach (array_keys($selectParts) as $key => $partName) {
+ $this->selectMock
+ ->expects($this->at($key))
+ ->method('setPart')
+ ->with($partName, $expectedParts[$partName]);
+ }
+
+ $this->assertSame($this->selectMock, $this->selectHydrator->recreate($selectParts));
+ }
+
+ /**
+ * @return array
+ */
+ public function recreateWithExpressionDataProvider()
+ {
+ $expressionMock = $this->getMockBuilder(JsonSerializableExpression::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return [
+ 'Select without expressions' => [
+ 'Parts' => [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ [
+ 'class' => 'Some_class',
+ 'arguments' => [
+ 'expression' => ['some(expression)']
+ ]
+ ],
+ 'alias_2',
+ ],
+ ]
+ ],
+ 'expectedParts' => [
+ Select::COLUMNS => [
+ [
+ 'table_name',
+ 'field_name',
+ 'alias',
+ ],
+ [
+ 'table_name',
+ $expressionMock,
+ 'alias_2',
+ ],
+ ]
+ ],
+ 'expectedExpressions' => [
+ $expressionMock
+ ]
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Analytics/composer.json b/app/code/Magento/Analytics/composer.json
new file mode 100644
index 0000000000000..6ff7850a95498
--- /dev/null
+++ b/app/code/Magento/Analytics/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "magento/module-analytics",
+ "description": "N/A",
+ "require": {
+ "php": "~7.0.13|~7.1.0",
+ "magento/module-backend": "100.2.*",
+ "magento/module-config": "101.0.*",
+ "magento/module-integration": "100.2.*",
+ "magento/module-store": "100.2.*",
+ "magento/framework": "101.0.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\Analytics\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/Analytics/docs/images/M2_MA_signup.png b/app/code/Magento/Analytics/docs/images/M2_MA_signup.png
new file mode 100644
index 0000000000000..78ed8fad92881
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/M2_MA_signup.png differ
diff --git a/app/code/Magento/Analytics/docs/images/analytics_modules.png b/app/code/Magento/Analytics/docs/images/analytics_modules.png
new file mode 100644
index 0000000000000..0bf6048b0d9cc
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/analytics_modules.png differ
diff --git a/app/code/Magento/Analytics/docs/images/data_transition.png b/app/code/Magento/Analytics/docs/images/data_transition.png
new file mode 100644
index 0000000000000..a75e97983e15d
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/data_transition.png differ
diff --git a/app/code/Magento/Analytics/docs/images/definition.png b/app/code/Magento/Analytics/docs/images/definition.png
new file mode 100644
index 0000000000000..16acc576320b0
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/definition.png differ
diff --git a/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png b/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png
new file mode 100644
index 0000000000000..f39d2e4900703
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/mbi_file_exchange.png differ
diff --git a/app/code/Magento/Analytics/docs/images/signup.png b/app/code/Magento/Analytics/docs/images/signup.png
new file mode 100644
index 0000000000000..561e18b3a351f
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/signup.png differ
diff --git a/app/code/Magento/Analytics/docs/images/update.png b/app/code/Magento/Analytics/docs/images/update.png
new file mode 100644
index 0000000000000..149f5b5d3f9bd
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/update.png differ
diff --git a/app/code/Magento/Analytics/docs/images/update_request.png b/app/code/Magento/Analytics/docs/images/update_request.png
new file mode 100644
index 0000000000000..7181251e3634e
Binary files /dev/null and b/app/code/Magento/Analytics/docs/images/update_request.png differ
diff --git a/app/code/Magento/Analytics/etc/acl.xml b/app/code/Magento/Analytics/etc/acl.xml
new file mode 100644
index 0000000000000..bf2251895f929
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/acl.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/di.xml b/app/code/Magento/Analytics/etc/adminhtml/di.xml
new file mode 100644
index 0000000000000..5e305e70e5ad3
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/di.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ - Magento\Analytics\Model\System\Message\NotificationAboutFailedSubscription
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/menu.xml b/app/code/Magento/Analytics/etc/adminhtml/menu.xml
new file mode 100644
index 0000000000000..915211c4bb85e
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/menu.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/routes.xml b/app/code/Magento/Analytics/etc/adminhtml/routes.xml
new file mode 100644
index 0000000000000..0ae2762dacc5f
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/routes.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/adminhtml/system.xml b/app/code/Magento/Analytics/etc/adminhtml/system.xml
new file mode 100644
index 0000000000000..889517e629e04
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Advanced Reporting
+ general
+ Magento_Analytics::analytics_settings
+
+ Advanced Reporting
+ For more information, see our
+ terms and conditions .]]>
+
+ Advanced Reporting Service
+ Magento\Config\Model\Config\Source\Enabledisable
+ Magento\Analytics\Model\Config\Backend\Enabled
+ Magento\Analytics\Block\Adminhtml\System\Config\SubscriptionStatusLabel
+ analytics/subscription/enabled
+
+
+ Time of day to send data
+ Magento\Analytics\Block\Adminhtml\System\Config\CollectionTimeLabel
+ Magento\Analytics\Model\Config\Backend\CollectionTime
+
+
+ Industry Data
+ Industry
+ In order to personalize your Advanced Reporting experience, please select your industry.
+ Magento\Analytics\Model\Config\Source\Vertical
+ Magento\Analytics\Model\Config\Backend\Vertical
+ Magento\Analytics\Block\Adminhtml\System\Config\Vertical
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xml b/app/code/Magento/Analytics/etc/analytics.xml
new file mode 100644
index 0000000000000..77ebe751a31cf
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/analytics.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ modules
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stores
+
+
+
+
+
+
+
+
+ websites
+
+
+
+
+
+
+
+
+ groups
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/analytics.xsd b/app/code/Magento/Analytics/etc/analytics.xsd
new file mode 100644
index 0000000000000..2506e3d6a6a9a
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/analytics.xsd
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ File name attribute can has only [a-zA-Z0-9/_].
+
+
+
+
+
+
+
+
+
+ Value is required.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml
new file mode 100644
index 0000000000000..b6194ba12993f
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/config.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ https://advancedreporting.rjmetrics.com/signup
+ https://advancedreporting.rjmetrics.com/update
+ https://dashboard.rjmetrics.com/v2/magento/signup
+ https://advancedreporting.rjmetrics.com/otp
+ https://advancedreporting.rjmetrics.com/report
+ https://advancedreporting.rjmetrics.com/report
+
+ Magento Analytics user
+
+ 02,00,00
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/crontab.xml b/app/code/Magento/Analytics/etc/crontab.xml
new file mode 100644
index 0000000000000..a4beef0359540
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/crontab.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/di.xml b/app/code/Magento/Analytics/etc/di.xml
new file mode 100644
index 0000000000000..b9bb9cc9ff00c
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/di.xml
@@ -0,0 +1,275 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\SignUpCommand
+ - Magento\Analytics\Model\Connector\UpdateCommand
+ - Magento\Analytics\Model\Connector\NotifyDataChangedCommand
+
+
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Data
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Reader
+ Magento_Analytics_ReportXml_CacheId
+
+
+
+
+ urn:magento:module:Magento_Analytics:etc/reports.xsd
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Converter\Xml
+ Magento\Analytics\ReportXml\Config\SchemaLocator
+ reports.xml
+
+ - name
+ -
+
- name
+ - alias
+
+ - name
+ - name
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+ - glue
+ -
+
- attribute
+ - operator
+
+ - glue
+ -
+
- attribute
+ - operator
+
+
+
+
+
+
+
+ - Magento\Analytics\ReportXml\Config\Reader\Xml
+
+
+
+
+
+
+ Magento\Analytics\Model\Config\Data
+
+
+
+
+ Magento\Analytics\Model\Config\Reader
+ Magento_Analytics_CacheId
+
+
+
+
+ urn:magento:module:Magento_Analytics:etc/analytics.xsd
+
+
+
+
+ Magento\Analytics\ReportXml\Config\Converter\Xml
+ Magento\Analytics\Model\Config\SchemaLocator
+ analytics.xml
+
+ - name
+
+
+
+
+
+
+
+ - Magento\Analytics\ReportXml\DB\Assembler\FromAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\FilterAssembler
+ - Magento\Analytics\ReportXml\DB\Assembler\JoinAssembler
+
+
+
+
+
+
+ - Magento\Analytics\Model\Config\Reader\Xml
+
+
+
+
+
+
+ - web/unsecure/base_url
+ - currency/options/base
+ - general/locale/timezone
+ - general/country/default
+ - carriers/dhl/title
+ - carriers/dhl/active
+ - carriers/fedex/title
+ - carriers/fedex/active
+ - carriers/flatrate/title
+ - carriers/flatrate/active
+ - carriers/tablerate/title
+ - carriers/tablerate/active
+ - carriers/freeshipping/title
+ - carriers/freeshipping/active
+ - carriers/ups/title
+ - carriers/ups/active
+ - carriers/usps/title
+ - carriers/usps/active
+ - payment/free/title
+ - payment/free/active
+ - payment/checkmo/title
+ - payment/checkmo/active
+ - payment/purchaseorder/title
+ - payment/purchaseorder/active
+ - payment/banktransfer/title
+ - payment/banktransfer/active
+ - payment/cashondelivery/title
+ - payment/cashondelivery/active
+ - payment/authorizenet_directpost/title
+ - payment/authorizenet_directpost/active
+ - payment/paypal_billing_agreement/title
+ - payment/paypal_billing_agreement/active
+ - payment/braintree/title
+ - payment/braintree/active
+ - payment/braintree_paypal/title
+ - payment/braintree_paypal/active
+ - analytics/general/vertical
+
+
+
+
+
+
+ - Apps and Games
+ - Athletic/Sporting Goods
+ - Art and Design
+ - Auto Parts
+ - Baby/Children’s Apparel, Gear and Toys
+ - Beauty and Cosmetics
+ - Books, Music and Magazines
+ - Crafts and Stationery
+ - Consumer Electronics
+ - Deal Site
+ - Fashion Apparel and Accessories
+ - Food, Beverage and Grocery
+ - Home Goods and Furniture
+ - Home Improvement
+ - Jewelry and Watches
+ - Mass Merchant
+ - Office Supplies
+ - Outdoor and Camping Gear
+ - Pet Goods
+ - Pharma and Medical Devices
+ - Technology B2B
+ - Other
+
+
+
+
+
+
+
+
+
+ - \Magento\Analytics\Model\Connector\ResponseHandler\SignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\Update
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\OTP
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+
+ - Magento\Analytics\Model\Connector\ResponseHandler\ReSignUp
+
+
+
+
+
+ SignUpResponseResolver
+
+
+
+
+ UpdateResponseResolver
+
+
+
+
+ OtpResponseResolver
+
+
+
+
+ NotifyDataChangedResponseResolver
+
+
+
+
+
+ - 1
+ - 1
+ - 1
+ - 1
+ - 1
+ - 1
+ - 1
+
+
+ - 1
+ - 1
+ - 1
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/module.xml b/app/code/Magento/Analytics/etc/module.xml
new file mode 100644
index 0000000000000..32ee5d23a4d86
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/module.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/reports.xml b/app/code/Magento/Analytics/etc/reports.xml
new file mode 100644
index 0000000000000..8a43658670293
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/reports.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/Analytics/etc/reports.xsd b/app/code/Magento/Analytics/etc/reports.xsd
new file mode 100644
index 0000000000000..d0ba4068244fe
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/reports.xsd
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/etc/webapi.xml b/app/code/Magento/Analytics/etc/webapi.xml
new file mode 100644
index 0000000000000..8252d039f1d03
--- /dev/null
+++ b/app/code/Magento/Analytics/etc/webapi.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/i18n/en_US.csv b/app/code/Magento/Analytics/i18n/en_US.csv
new file mode 100644
index 0000000000000..516c388feb823
--- /dev/null
+++ b/app/code/Magento/Analytics/i18n/en_US.csv
@@ -0,0 +1,84 @@
+"Subscription status","Subscription status"
+"Sorry, there has been an error processing your request. Please try again later.","Sorry, there has been an error processing your request. Please try again later."
+"Sorry, there was an error processing your registration request to Magento Analytics. Please try again later.","Sorry, there was an error processing your registration request to Magento Analytics. Please try again later."
+"Error occurred during postponement notification","Error occurred during postponement notification"
+"Time value has an unsupported format","Time value has an unsupported format"
+"Cron settings can't be saved","Cron settings can't be saved"
+"There was an error save new configuration value.","There was an error save new configuration value."
+"Please select a vertical.","Please select a vertical."
+"--Please Select--","--Please Select--"
+"Command was not found.","Command was not found."
+"Input data must be string or convertible into string.","Input data must be string or convertible into string."
+"Input data must be non-empty string.","Input data must be non-empty string."
+"Not valid cipher method.","Not valid cipher method."
+"Encryption key can't be empty.","Encryption key can't be empty."
+"Source ""%1"" is not exist","Source ""%1"" is not exist"
+"These arguments can't be empty ""%1""","These arguments can't be empty ""%1"""
+"Cannot find predefined integration user!","Cannot find predefined integration user!"
+"File is not ready yet.","File is not ready yet."
+"Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later.","Your Base URL has been changed and your reports are being updated. Advanced Reporting will be available once this change has been processed. Please try again later."
+"Failed to synchronize data to the Magento Business Intelligence service. ","Failed to synchronize data to the Magento Business Intelligence service. "
+"Retry Synchronization ","Retry Synchronization "
+TestMessage,TestMessage
+"Error message","Error message"
+"Apps and Games","Apps and Games"
+"Athletic/Sporting Goods","Athletic/Sporting Goods"
+"Art and Design","Art and Design"
+"Advanced Reporting","Advanced Reporting"
+"Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data.","Gain new insights and take command of your business' performance, using our dynamic product, order, and customer reports tailored to your customer data."
+"View details","View details"
+"Go to Advanced Reporting","Go to Advanced Reporting"
+"An error occurred while subscription process.","An error occurred while subscription process."
+Analytics,Analytics
+API,API
+Configuration,Configuration
+"Business Intelligence","Business Intelligence"
+"BI Essentials","BI Essentials"
+"This service provides a dynamic suite of reports with rich insights about your business.
+ Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the
+ ""Go to Advanced Reporting"" link. For more information, see our
+ terms and conditions .
+ ","This service provides a dynamic suite of reports with rich insights about your business.
+ Your reports can be accessed securely on a personalized dashboard outside of the admin panel by clicking on the
+ ""Go to Advanced Reporting"" link. For more information, see our
+ terms and conditions ."
+"Advanced Reporting Service","Advanced Reporting Service"
+Industry,Industry
+"Time of day to send data","Time of day to send data"
+"Get more insights from Magento Business Intelligence ","Get more insights from Magento Business Intelligence "
+"Magento Business Intelligence provides you with a simple and clear path to
+ becoming more data driven. Learn more about BI Essentials tier.","Magento Business Intelligence provides you with a simple and clear path to
+ becoming more data driven. Learn more about BI Essentials tier."
+"Auto Parts","Auto Parts"
+"Baby/Children’s Apparel, Gear and Toys","Baby/Children’s Apparel, Gear and Toys"
+"Beauty and Cosmetics","Beauty and Cosmetics"
+"Books, Music and Magazines","Books, Music and Magazines"
+"Crafts and Stationery","Crafts and Stationery"
+"Consumer Electronics","Consumer Electronics"
+"Deal Site","Deal Site"
+"Fashion Apparel and Accessories","Fashion Apparel and Accessories"
+"Food, Beverage and Grocery","Food, Beverage and Grocery"
+"Home Goods and Furniture","Home Goods and Furniture"
+"Home Improvement","Home Improvement"
+"Jewelry and Watches","Jewelry and Watches"
+"Mass Merchant","Mass Merchant"
+"Office Supplies","Office Supplies"
+"Outdoor and Camping Gear","Outdoor and Camping Gear"
+"Pet Goods","Pet Goods"
+"Pharma and Medical Devices","Pharma and Medical Devices"
+"Technology B2B","Technology B2B"
+"Analytics Subscription","Analytics Subscription"
+"powered by Magento Business Intelligence","powered by Magento Business Intelligence"
+"Are you sure you want to opt out?","Are you sure you want to opt out?"
+Cancel,Cancel
+"Opt out","Opt out"
+"Advanced Reporting in included,
+ free of charge, in your Magento software. When you opt out, we collect no product, order, and
+ customer data to generate our dynamic reports.
To opt in later: You can always turn on Advanced
+ Reporting in you Admin Panel.
","Advanced Reporting in included,
+ free of charge, in your Magento software. When you opt out, we collect no product, order, and
+ customer data to generate our dynamic reports.
To opt in later: You can always turn on Advanced
+ Reporting in you Admin Panel.
"
+"In order to personalize your Advanced Reporting experience, please select your industry.","In order to personalize your Advanced Reporting experience, please select your industry."
diff --git a/app/code/Magento/Analytics/registration.php b/app/code/Magento/Analytics/registration.php
new file mode 100644
index 0000000000000..58d3688b7491d
--- /dev/null
+++ b/app/code/Magento/Analytics/registration.php
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml
new file mode 100644
index 0000000000000..a22c603b2a8b3
--- /dev/null
+++ b/app/code/Magento/Analytics/view/adminhtml/templates/dashboard/section.phtml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ = $block->escapeHtml(__('Advanced Reporting')) ?>
+
+
+ = $block->escapeHtml(__('Gain new insights and take command of your business\' performance,' .
+ ' using our dynamic product, order, and customer reports tailored to your customer data.')) ?>
+
+
+
+
diff --git a/app/code/Magento/Authorization/composer.json b/app/code/Magento/Authorization/composer.json
index c0b0f9c6b13df..22483b7961841 100644
--- a/app/code/Magento/Authorization/composer.json
+++ b/app/code/Magento/Authorization/composer.json
@@ -2,7 +2,7 @@
"name": "magento/module-authorization",
"description": "Authorization module provides access to Magento ACL functionality.",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-backend": "100.2.*",
"magento/framework": "101.0.*"
},
diff --git a/app/code/Magento/Authorizenet/composer.json b/app/code/Magento/Authorizenet/composer.json
index 1022bd47a5786..c389a1c6c4dfa 100644
--- a/app/code/Magento/Authorizenet/composer.json
+++ b/app/code/Magento/Authorizenet/composer.json
@@ -2,7 +2,7 @@
"name": "magento/module-authorizenet",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-sales": "101.0.*",
"magento/module-store": "100.2.*",
"magento/module-quote": "101.0.*",
diff --git a/app/code/Magento/Backend/Block/Cache.php b/app/code/Magento/Backend/Block/Cache.php
index e14358396aa70..82c36bf3a1fe4 100644
--- a/app/code/Magento/Backend/Block/Cache.php
+++ b/app/code/Magento/Backend/Block/Cache.php
@@ -22,24 +22,29 @@ protected function _construct()
$this->_headerText = __('Cache Storage Management');
parent::_construct();
$this->buttonList->remove('add');
- $this->buttonList->add(
- 'flush_magento',
- [
- 'label' => __('Flush Magento Cache'),
- 'onclick' => 'setLocation(\'' . $this->getFlushSystemUrl() . '\')',
- 'class' => 'primary flush-cache-magento'
- ]
- );
- $message = __('The cache storage may contain additional data. Are you sure that you want to flush it?');
- $this->buttonList->add(
- 'flush_system',
- [
- 'label' => __('Flush Cache Storage'),
- 'onclick' => 'confirmSetLocation(\'' . $message . '\', \'' . $this->getFlushStorageUrl() . '\')',
- 'class' => 'flush-cache-storage'
- ]
- );
+ if ($this->_authorization->isAllowed('Magento_Backend::flush_magento_cache')) {
+ $this->buttonList->add(
+ 'flush_magento',
+ [
+ 'label' => __('Flush Magento Cache'),
+ 'onclick' => 'setLocation(\'' . $this->getFlushSystemUrl() . '\')',
+ 'class' => 'primary flush-cache-magento'
+ ]
+ );
+ }
+
+ if ($this->_authorization->isAllowed('Magento_Backend::flush_cache_storage')) {
+ $message = __('The cache storage may contain additional data. Are you sure that you want to flush it?');
+ $this->buttonList->add(
+ 'flush_system',
+ [
+ 'label' => __('Flush Cache Storage'),
+ 'onclick' => 'confirmSetLocation(\'' . $message . '\', \'' . $this->getFlushStorageUrl() . '\')',
+ 'class' => 'flush-cache-storage'
+ ]
+ );
+ }
}
/**
diff --git a/app/code/Magento/Backend/Block/Cache/Permissions.php b/app/code/Magento/Backend/Block/Cache/Permissions.php
new file mode 100644
index 0000000000000..272a603145f09
--- /dev/null
+++ b/app/code/Magento/Backend/Block/Cache/Permissions.php
@@ -0,0 +1,62 @@
+authorization = $authorization;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasAccessToFlushCatalogImages()
+ {
+ return $this->authorization->isAllowed('Magento_Backend::flush_catalog_images');
+ }
+ /**
+ * @return bool
+ */
+ public function hasAccessToFlushJsCss()
+ {
+ return $this->authorization->isAllowed('Magento_Backend::flush_js_css');
+ }
+ /**
+ * @return bool
+ */
+ public function hasAccessToFlushStaticFiles()
+ {
+ return $this->authorization->isAllowed('Magento_Backend::flush_static_files');
+ }
+ /**
+ * @return bool
+ */
+ public function hasAccessToAdditionalActions()
+ {
+ return ($this->hasAccessToFlushCatalogImages()
+ || $this->hasAccessToFlushJsCss()
+ || $this->hasAccessToFlushStaticFiles());
+ }
+}
diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php
index cecd7b8050352..301dffbdc4987 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Graph.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php
@@ -421,6 +421,8 @@ public function getChartUrl($directUrl = true)
$tmpstring = implode('|', $this->_axisLabels[$idx]);
$valueBuffer[] = $indexid . ":|" . $tmpstring;
+ } elseif ($idx == 'y') {
+ $valueBuffer[] = $indexid . ":|" . implode('|', $yLabels);
}
$indexid++;
}
diff --git a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php
index 9d9409fba093b..50279786c0a5b 100644
--- a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php
+++ b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php
@@ -92,7 +92,7 @@ protected function _prepareCollection()
protected function _afterLoadCollection()
{
foreach ($this->getCollection() as $item) {
- $item->getCustomer() ?: $item->setCustomer('Guest');
+ $item->getCustomer() ?: $item->setCustomer($item->getBillingAddress()->getName());
}
return $this;
}
diff --git a/app/code/Magento/Backend/Block/GlobalSearch.php b/app/code/Magento/Backend/Block/GlobalSearch.php
index f4a46283808f4..9af4e9faef761 100644
--- a/app/code/Magento/Backend/Block/GlobalSearch.php
+++ b/app/code/Magento/Backend/Block/GlobalSearch.php
@@ -3,19 +3,61 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Backend\Block;
+use Magento\Backend\Model\GlobalSearch\SearchEntityFactory;
+use Magento\Backend\Model\GlobalSearch\SearchEntity;
+use Magento\Framework\App\ObjectManager;
+
/**
* @api
* @since 100.0.2
*/
class GlobalSearch extends \Magento\Backend\Block\Template
{
+ /**
+ * @var SearchEntityFactory
+ */
+ private $searchEntityFactory;
+
/**
* @var string
*/
protected $_template = 'Magento_Backend::system/search.phtml';
+ /**
+ * @var array
+ */
+ private $entityResources;
+
+ /**
+ * @var array
+ */
+ private $entityPaths;
+
+ /**
+ * @param Template\Context $context
+ * @param array $data
+ * @param array $entityResources
+ * @param array $entityPaths
+ * @param SearchEntityFactory|null $searchEntityFactory
+ */
+ public function __construct(
+ Template\Context $context,
+ array $data = [],
+ array $entityResources = [],
+ array $entityPaths = [],
+ SearchEntityFactory $searchEntityFactory = null
+ ) {
+ $this->entityResources = $entityResources;
+ $this->entityPaths = $entityPaths;
+ $this->searchEntityFactory = $searchEntityFactory ?: ObjectManager::getInstance()
+ ->get(SearchEntityFactory::class);
+
+ parent::__construct($context, $data);
+ }
+
/**
* Get components configuration
* @return array
@@ -34,4 +76,48 @@ public function getWidgetInitOptions()
]
];
}
+
+ /**
+ * Get entities which are allowed to show.
+ *
+ * @return SearchEntity[]
+ */
+ public function getEntitiesToShow()
+ {
+ $allowedEntityTypes = [];
+ $entitiesToShow = [];
+
+ foreach ($this->entityResources as $entityType => $resource) {
+ if ($this->getAuthorization()->isAllowed($resource)) {
+ $allowedEntityTypes[] = $entityType;
+ }
+ }
+
+ foreach ($allowedEntityTypes as $entityType) {
+ $url = $this->getUrlEntityType($entityType);
+
+ $searchEntity = $this->searchEntityFactory->create();
+ $searchEntity->setId('searchPreview' . $entityType);
+ $searchEntity->setTitle('in ' . $entityType);
+ $searchEntity->setUrl($url);
+
+ $entitiesToShow[] = $searchEntity;
+ }
+
+ return $entitiesToShow;
+ }
+
+ /**
+ * Get url path by entity type.
+ *
+ * @param string $entityType
+ *
+ * @return string
+ */
+ private function getUrlEntityType(string $entityType)
+ {
+ $urlPath = $this->entityPaths[$entityType] ?? '';
+
+ return $this->getUrl($urlPath);
+ }
}
diff --git a/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php b/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php
index f19799d2e4939..034887c67d1ee 100644
--- a/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php
+++ b/app/code/Magento/Backend/Block/System/Store/Edit/AbstractForm.php
@@ -37,7 +37,7 @@ protected function _prepareForm()
['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']]
);
- $this->_prepareStoreFieldSet($form);
+ $this->_prepareStoreFieldset($form);
$form->addField(
'store_type',
diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php
index 59657f38465d7..3d7154eb20f92 100644
--- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php
+++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php
@@ -27,6 +27,7 @@ public function render(\Magento\Framework\DataObject $row)
$this->getUrl('adminhtml/*/editGroup', ['group_id' => $row->getGroupId()]) .
'">' .
$this->escapeHtml($row->getData($this->getColumn()->getIndex())) .
- '';
+ ' '
+ . '(' . __('Code') . ': ' . $row->getGroupCode() . ')';
}
}
diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php
index 23b2de683a958..9cfc8bfc52691 100644
--- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php
+++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Store.php
@@ -27,6 +27,7 @@ public function render(\Magento\Framework\DataObject $row)
$this->getUrl('adminhtml/*/editStore', ['store_id' => $row->getStoreId()]) .
'">' .
$this->escapeHtml($row->getData($this->getColumn()->getIndex())) .
- '';
+ ' ' .
+ '(' . __('Code') . ': ' . $row->getStoreCode() . ')';
}
}
diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php
index 913e2c903d20c..487eb4f8acfda 100644
--- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php
+++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Website.php
@@ -24,6 +24,7 @@ public function render(\Magento\Framework\DataObject $row)
$this->getUrl('adminhtml/*/editWebsite', ['website_id' => $row->getWebsiteId()]) .
'">' .
$this->escapeHtml($row->getData($this->getColumn()->getIndex())) .
- '';
+ ' ' .
+ '(' . __('Code') . ': ' . $row->getCode() . ')';
}
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php
index 40a5d92c56b6f..632603d389d21 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php
@@ -127,7 +127,7 @@ public function getHtml()
/**
* @param string|null $index
- * @return string
+ * @return array|string|int|float|null
*/
public function getEscapedValue($index = null)
{
@@ -138,6 +138,11 @@ public function getEscapedValue($index = null)
$this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT)
);
}
+
+ if (is_string($value)) {
+ return $this->escapeHtml($value);
+ }
+
return $value;
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php
index 96b3471db845e..a5e4a34389671 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php
@@ -140,8 +140,8 @@ public function getHtml()
/**
* Return escaped value for calendar
*
- * @param string $index
- * @return string
+ * @param string|null $index
+ * @return array|string|int|float|null
*/
public function getEscapedValue($index = null)
{
@@ -150,6 +150,11 @@ public function getEscapedValue($index = null)
if ($value instanceof \DateTimeInterface) {
return $this->_localeDate->formatDateTime($value);
}
+
+ if (is_string($value)) {
+ return $this->escapeHtml($value);
+ }
+
return $value;
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php
index 2cbe264c5f396..479a2b6b20293 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Radio.php
@@ -31,8 +31,7 @@ public function getCondition()
{
if ($this->getValue()) {
return $this->getColumn()->getValue();
- } else {
- return [['neq' => $this->getColumn()->getValue()], ['is' => new \Zend_Db_Expr('NULL')]];
}
+ return [['neq' => $this->getColumn()->getValue()], ['is' => new \Zend_Db_Expr('NULL')]];
}
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php
index d9b00d2ba2503..662cbedaed8db 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php
@@ -3,8 +3,15 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Backend\Block\Widget\Grid;
+use Magento\Backend\Block\Template\Context;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\AuthorizationInterface;
+use Magento\Framework\DataObject;
+use Magento\Framework\Json\EncoderInterface;
+
/**
* Grid widget massaction default block
*
@@ -14,4 +21,72 @@
*/
class Massaction extends \Magento\Backend\Block\Widget\Grid\Massaction\AbstractMassaction
{
+ /**
+ * @var AuthorizationInterface
+ */
+ private $authorization;
+
+ /**
+ * Map bind item id to a particular acl type
+ * itemId => acl
+ *
+ * @var array
+ */
+ private $restrictions = [
+ 'enable' => 'Magento_Backend::toggling_cache_type',
+ 'disable' => 'Magento_Backend::toggling_cache_type',
+ 'refresh' => 'Magento_Backend::refresh_cache_type',
+ ];
+
+ /**
+ * Massaction constructor.
+ *
+ * @param Context $context
+ * @param EncoderInterface $jsonEncoder
+ * @param array $data
+ * @param AuthorizationInterface $authorization
+ */
+ public function __construct(
+ Context $context,
+ EncoderInterface $jsonEncoder,
+ array $data = [],
+ AuthorizationInterface $authorization = null
+ ) {
+ $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class);
+
+ parent::__construct($context, $jsonEncoder, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $itemId
+ * @param array|DataObject $item
+ *
+ * @return $this
+ */
+ public function addItem($itemId, $item)
+ {
+ if (!$this->isRestricted($itemId)) {
+ parent::addItem($itemId, $item);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Check if access to action restricted
+ *
+ * @param string $itemId
+ *
+ * @return bool
+ */
+ private function isRestricted(string $itemId): bool
+ {
+ if (!key_exists($itemId, $this->restrictions)) {
+ return false;
+ }
+
+ return !$this->authorization->isAllowed($this->restrictions[$itemId]);
+ }
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php
index 8252ed1a1e2f8..e790cbfa69d8a 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php
@@ -222,9 +222,8 @@ public function getSelectedJson()
if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) {
$selected = explode(',', $selected);
return join(',', $selected);
- } else {
- return '';
}
+ return '';
}
/**
@@ -237,9 +236,8 @@ public function getSelected()
if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) {
$selected = explode(',', $selected);
return $selected;
- } else {
- return [];
}
+ return [];
}
/**
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php
index 42f5e61bf5fa8..eae1a137e927d 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php
@@ -218,9 +218,8 @@ public function getSelectedJson()
if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) {
$selected = explode(',', $selected);
return join(',', $selected);
- } else {
- return '';
}
+ return '';
}
/**
@@ -233,9 +232,8 @@ public function getSelected()
if ($selected = $this->getRequest()->getParam($this->getFormFieldNameInternal())) {
$selected = explode(',', $selected);
return $selected;
- } else {
- return [];
}
+ return [];
}
/**
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php
index b34e4d9c84939..e1ea57f63035e 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php
@@ -50,9 +50,8 @@ public function execute()
// redirect according to rewrite rule
if ($requestUrl != $backendUrl) {
return $this->getRedirect($backendUrl);
- } else {
- return $this->resultPageFactory->create();
}
+ return $this->resultPageFactory->create();
}
/**
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php
index 1895bd08c464f..7a926b1c09c3e 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php
@@ -11,6 +11,13 @@
class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_catalog_images';
+
/**
* Clean JS/css files cache
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php
index 521cb9806a135..72f23ab65cf8a 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php
@@ -11,6 +11,13 @@
class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_js_css';
+
/**
* Clean JS/css files cache
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php
index adfbf7cfe63c8..27ae2fc31e150 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php
@@ -10,6 +10,13 @@
class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_static_files';
+
/**
* Clean static files cache
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php
index 2f9dc9ad6a7a5..ca89ea58fa6f3 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php
@@ -8,6 +8,13 @@
class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_cache_storage';
+
/**
* Flush cache storage
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php
index 0f55a59353d65..f0fed159e0f22 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php
@@ -8,6 +8,13 @@
class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::flush_magento_cache';
+
/**
* Flush all magento cache
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php
index 204105852b9f1..2bfa937b06b77 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassDisable.php
@@ -16,6 +16,13 @@
*/
class MassDisable extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::toggling_cache_type';
+
/**
* @var State
*/
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php
index 32acf47887c44..113e0f2d8961b 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassEnable.php
@@ -16,6 +16,13 @@
*/
class MassEnable extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::toggling_cache_type';
+
/**
* @var State
*/
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php
index e18aa1555e11b..3843b030afb3d 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/MassRefresh.php
@@ -11,6 +11,13 @@
class MassRefresh extends \Magento\Backend\Controller\Adminhtml\Cache
{
+ /**
+ * Authorization level of a basic admin session
+ *
+ * @see _isAllowed()
+ */
+ const ADMIN_RESOURCE = 'Magento_Backend::refresh_cache_type';
+
/**
* Mass action for cache refresh
*
diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php
index c9bce1cbf3888..1b10c151a9d21 100644
--- a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php
+++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Save.php
@@ -32,9 +32,8 @@ private function getSecurityCookie()
{
if (!($this->securityCookie instanceof SecurityCookie)) {
return \Magento\Framework\App\ObjectManager::getInstance()->get(SecurityCookie::class);
- } else {
- return $this->securityCookie;
}
+ return $this->securityCookie;
}
/**
@@ -54,9 +53,9 @@ public function execute()
$user = $this->_objectManager->create(\Magento\User\Model\User::class)->load($userId);
$user->setId($userId)
- ->setUsername($this->getRequest()->getParam('username', false))
- ->setFirstname($this->getRequest()->getParam('firstname', false))
- ->setLastname($this->getRequest()->getParam('lastname', false))
+ ->setUserName($this->getRequest()->getParam('username', false))
+ ->setFirstName($this->getRequest()->getParam('firstname', false))
+ ->setLastName($this->getRequest()->getParam('lastname', false))
->setEmail(strtolower($this->getRequest()->getParam('email', false)));
if ($this->_objectManager->get(\Magento\Framework\Validator\Locale::class)->isValid($interfaceLocale)) {
diff --git a/app/code/Magento/Backend/Model/Auth/StorageInterface.php b/app/code/Magento/Backend/Model/Auth/StorageInterface.php
index 52b2b089c71e1..e643165a93317 100644
--- a/app/code/Magento/Backend/Model/Auth/StorageInterface.php
+++ b/app/code/Magento/Backend/Model/Auth/StorageInterface.php
@@ -23,7 +23,7 @@ interface StorageInterface
public function processLogin();
/**
- * Perform login specific actions
+ * Perform logout specific actions
*
* @return $this
* @abstract
diff --git a/app/code/Magento/Backend/Model/GlobalSearch/SearchEntity.php b/app/code/Magento/Backend/Model/GlobalSearch/SearchEntity.php
new file mode 100644
index 0000000000000..18691802b9218
--- /dev/null
+++ b/app/code/Magento/Backend/Model/GlobalSearch/SearchEntity.php
@@ -0,0 +1,73 @@
+getData('id');
+ }
+
+ /**
+ * Get url.
+ *
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->getData('url');
+ }
+
+ /**
+ * Get title.
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->getData('title');
+ }
+
+ /**
+ * Set Id.
+ *
+ * @param string $value
+ */
+ public function setId(string $value)
+ {
+ $this->setData('id', $value);
+ }
+
+ /**
+ * Set url.
+ *
+ * @param string $value
+ */
+ public function setUrl(string $value)
+ {
+ $this->setData('url', $value);
+ }
+
+ /**
+ * Set title.
+ *
+ * @param string $value
+ */
+ public function setTitle(string $value)
+ {
+ $this->setData('title', $value);
+ }
+}
diff --git a/app/code/Magento/Backend/Model/Menu/Item.php b/app/code/Magento/Backend/Model/Menu/Item.php
index fe6564d24e891..67c6216cbbc06 100644
--- a/app/code/Magento/Backend/Model/Menu/Item.php
+++ b/app/code/Magento/Backend/Model/Menu/Item.php
@@ -467,15 +467,15 @@ public function toArray()
{
return [
'parent_id' => $this->_parentId,
- 'module_name' => $this->_moduleName,
+ 'module' => $this->_moduleName,
'sort_index' => $this->_sortIndex,
- 'depends_on_config' => $this->_dependsOnConfig,
+ 'dependsOnConfig' => $this->_dependsOnConfig,
'id' => $this->_id,
'resource' => $this->_resource,
'path' => $this->_path,
'action' => $this->_action,
- 'depends_on_module' => $this->_dependsOnModule,
- 'tooltip' => $this->_tooltip,
+ 'dependsOnModule' => $this->_dependsOnModule,
+ 'toolTip' => $this->_tooltip,
'title' => $this->_title,
'target' => $this->target,
'sub_menu' => isset($this->_submenu) ? $this->_submenu->toArray() : null
@@ -492,23 +492,22 @@ public function toArray()
public function populateFromArray(array $data)
{
$this->_parentId = $this->_getArgument($data, 'parent_id');
- $this->_moduleName = $this->_getArgument($data, 'module_name', 'Magento_Backend');
+ $this->_moduleName = $this->_getArgument($data, 'module', 'Magento_Backend');
$this->_sortIndex = $this->_getArgument($data, 'sort_index');
- $this->_dependsOnConfig = $this->_getArgument($data, 'depends_on_config');
+ $this->_dependsOnConfig = $this->_getArgument($data, 'dependsOnConfig');
$this->_id = $this->_getArgument($data, 'id');
$this->_resource = $this->_getArgument($data, 'resource');
$this->_path = $this->_getArgument($data, 'path', '');
$this->_action = $this->_getArgument($data, 'action');
- $this->_dependsOnModule = $this->_getArgument($data, 'depends_on_module');
- $this->_tooltip = $this->_getArgument($data, 'tooltip', '');
+ $this->_dependsOnModule = $this->_getArgument($data, 'dependsOnModule');
+ $this->_tooltip = $this->_getArgument($data, 'toolTip');
$this->_title = $this->_getArgument($data, 'title');
$this->target = $this->_getArgument($data, 'target');
+ $this->_submenu = null;
if (isset($data['sub_menu'])) {
$menu = $this->_menuFactory->create();
$menu->populateFromArray($data['sub_menu']);
$this->_submenu = $menu;
- } else {
- $this->_submenu = null;
}
}
}
diff --git a/app/code/Magento/Backend/Model/Url.php b/app/code/Magento/Backend/Model/Url.php
index 48b443fe7ffd3..cf895268bcb7d 100644
--- a/app/code/Magento/Backend/Model/Url.php
+++ b/app/code/Magento/Backend/Model/Url.php
@@ -202,7 +202,7 @@ public function getUrl($routePath = null, $routeParams = null)
}
$cacheSecretKey = false;
- if (is_array($routeParams) && isset($routeParams['_cache_secret_key'])) {
+ if (isset($routeParams['_cache_secret_key'])) {
unset($routeParams['_cache_secret_key']);
$cacheSecretKey = true;
}
@@ -210,25 +210,22 @@ public function getUrl($routePath = null, $routeParams = null)
if (!$this->useSecretKey()) {
return $result;
}
+
$this->_setRoutePath($routePath);
$routeName = $this->_getRouteName('*');
$controllerName = $this->_getControllerName(self::DEFAULT_CONTROLLER_NAME);
$actionName = $this->_getActionName(self::DEFAULT_ACTION_NAME);
- if ($cacheSecretKey) {
- $secret = [self::SECRET_KEY_PARAM_NAME => "\${$routeName}/{$controllerName}/{$actionName}\$"];
- } else {
- $secret = [
- self::SECRET_KEY_PARAM_NAME => $this->getSecretKey($routeName, $controllerName, $actionName),
- ];
- }
- if (is_array($routeParams)) {
- $routeParams = array_merge($secret, $routeParams);
- } else {
- $routeParams = $secret;
- }
- if (is_array($this->_getRouteParams())) {
- $routeParams = array_merge($this->_getRouteParams(), $routeParams);
+
+ if (!isset($routeParams[self::SECRET_KEY_PARAM_NAME])) {
+ if (!is_array($routeParams)) {
+ $routeParams = [];
+ }
+ $secretKey = $cacheSecretKey
+ ? "\${$routeName}/{$controllerName}/{$actionName}\$"
+ : $this->getSecretKey($routeName, $controllerName, $actionName);
+ $routeParams[self::SECRET_KEY_PARAM_NAME] = $secretKey;
}
+
return parent::getUrl("{$routeName}/{$controllerName}/{$actionName}", $routeParams);
}
diff --git a/app/code/Magento/Backend/Model/Widget/Grid/Parser.php b/app/code/Magento/Backend/Model/Widget/Grid/Parser.php
index e2b77fb471acd..fbed7149aa565 100644
--- a/app/code/Magento/Backend/Model/Widget/Grid/Parser.php
+++ b/app/code/Magento/Backend/Model/Widget/Grid/Parser.php
@@ -30,8 +30,9 @@ public function parseExpression($expression)
$expression = trim($expression);
foreach ($this->_operations as $operation) {
$splittedExpr = preg_split('/\\' . $operation . '/', $expression, -1, PREG_SPLIT_DELIM_CAPTURE);
- if (count($splittedExpr) > 1) {
- for ($i = 0; $i < count($splittedExpr); $i++) {
+ $count = count($splittedExpr);
+ if ($count > 1) {
+ for ($i = 0; $i < $count; $i++) {
$stack = array_merge($stack, $this->parseExpression($splittedExpr[$i]));
if ($i > 0) {
$stack[] = $operation;
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php b/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php
new file mode 100644
index 0000000000000..6975b8ac092ad
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Unit/Block/Cache/PermissionsTest.php
@@ -0,0 +1,76 @@
+objectManager = new ObjectManager($this);
+
+ $this->mockAuthorization = $this->getMockBuilder(Authorization::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isAllowed'])
+ ->getMock();
+
+ $this->permissions = new Permissions($this->mockAuthorization);
+ }
+
+ public function testHasAccessToFlushCatalogImages()
+ {
+ $this->mockAuthorization->expects($this->atLeastOnce())
+ ->method('isAllowed')
+ ->with('Magento_Backend::flush_catalog_images')
+ ->willReturn(true);
+
+ $this->assertTrue($this->permissions->hasAccessToFlushCatalogImages());
+ }
+
+ public function testHasAccessToFlushJsCss()
+ {
+ $this->mockAuthorization->expects($this->atLeastOnce())
+ ->method('isAllowed')
+ ->with('Magento_Backend::flush_js_css')
+ ->willReturn(true);
+
+ $this->assertTrue($this->permissions->hasAccessToFlushJsCss());
+ }
+
+ public function testHasAccessToFlushStaticFiles()
+ {
+ $this->mockAuthorization->expects($this->atLeastOnce())
+ ->method('isAllowed')
+ ->with('Magento_Backend::flush_static_files')
+ ->willReturn(true);
+
+ $this->assertTrue($this->permissions->hasAccessToFlushStaticFiles());
+ }
+}
diff --git a/app/code/Magento/Backend/Test/Unit/Block/GlobalSearchTest.php b/app/code/Magento/Backend/Test/Unit/Block/GlobalSearchTest.php
new file mode 100644
index 0000000000000..0010ffad87524
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Unit/Block/GlobalSearchTest.php
@@ -0,0 +1,123 @@
+ \Magento\Catalog\Controller\Adminhtml\Product::ADMIN_RESOURCE,
+ 'Orders' => \Magento\Sales\Controller\Adminhtml\Order::ADMIN_RESOURCE,
+ 'Customers' => \Magento\Customer\Controller\Adminhtml\Index::ADMIN_RESOURCE,
+ 'Pages' => \Magento\Cms\Controller\Adminhtml\Page\Index::ADMIN_RESOURCE,
+ ];
+
+ /**
+ * @var array
+ */
+ private $entityPaths = [
+ 'Products' => 'catalog/product/index/',
+ 'Orders' => 'sales/order/index/',
+ 'Customers' => 'customer/index/index',
+ 'Pages' => 'cms/page/index/',
+ ];
+
+ protected function setUp()
+ {
+ $objectManager = new ObjectManager($this);
+
+ $this->authorization = $this->createMock(\Magento\Framework\AuthorizationInterface::class);
+ $this->urlBuilder = $this->createMock(\Magento\Framework\UrlInterface::class);
+ $context = $this->createMock(\Magento\Backend\Block\Template\Context::class);
+
+ $context->expects($this->atLeastOnce())->method('getAuthorization')->willReturn($this->authorization);
+ $context->expects($this->atLeastOnce())->method('getUrlBuilder')->willReturn($this->urlBuilder);
+
+ $this->searchEntityFactory = $this->createMock(\Magento\Backend\Model\GlobalSearch\SearchEntityFactory::class);
+
+ $this->globalSearch = $objectManager->getObject(
+ GlobalSearch::class,
+ [
+ 'context' => $context,
+ 'searchEntityFactory' => $this->searchEntityFactory,
+ 'entityResources' => $this->entityResources,
+ 'entityPaths' => $this->entityPaths,
+ ]
+ );
+ }
+
+ /**
+ * @param array $results
+ * @param int $expectedEntitiesQty
+ *
+ * @dataProvider getEntitiesToShowDataProvider
+ */
+ public function testGetEntitiesToShow(array $results, int $expectedEntitiesQty)
+ {
+ $searchEntity = $this->createMock(SearchEntity::class);
+
+ $this->authorization->expects($this->exactly(count($results)))->method('isAllowed')
+ ->willReturnOnConsecutiveCalls($results[0], $results[1], $results[2], $results[3]);
+ $this->urlBuilder->expects($this->exactly($expectedEntitiesQty))
+ ->method('getUrl')->willReturn('some/url/is/here');
+ $this->searchEntityFactory->expects($this->exactly($expectedEntitiesQty))
+ ->method('create')->willReturn($searchEntity);
+
+ $searchEntity->expects($this->exactly($expectedEntitiesQty))->method('setId');
+ $searchEntity->expects($this->exactly($expectedEntitiesQty))->method('setTitle');
+ $searchEntity->expects($this->exactly($expectedEntitiesQty))->method('setUrl');
+
+ $this->assertSame($expectedEntitiesQty, count($this->globalSearch->getEntitiesToShow()));
+ }
+
+ public function getEntitiesToShowDataProvider()
+ {
+ return [
+ [
+ [true, false, true, false],
+ 2,
+ ],
+ [
+ [true, true, true, true],
+ 4,
+ ],
+ [
+ [false, false, false, false],
+ 0,
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php
index be171a8ed40bf..df242a4cf6129 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnSetTest.php
@@ -117,7 +117,7 @@ public function testSetFilterTypePropagatesFilterTypeToColumns()
public function testGetRowUrlIfUrlPathNotSet()
{
- $this->assertEquals('#', $this->_block->getRowUrl(new \StdClass()));
+ $this->assertEquals('#', $this->_block->getRowUrl(new \stdClass()));
}
public function testGetRowUrl()
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php
index da13af87b71ea..c5c56fd75fbe7 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ColumnTest.php
@@ -351,7 +351,7 @@ public function testSetGetGrid()
$this->_block->setFilter('StdClass');
- $grid = new \StdClass();
+ $grid = new \stdClass();
$this->_block->setGrid($grid);
$this->assertEquals($grid, $this->_block->getGrid());
}
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php
index bb389a996e1ed..29ce448a04ecb 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php
@@ -10,6 +10,7 @@
namespace Magento\Backend\Test\Unit\Block\Widget\Grid;
use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker;
+use Magento\Framework\Authorization;
class MassactionTest extends \PHPUnit\Framework\TestCase
{
@@ -43,6 +44,11 @@ class MassactionTest extends \PHPUnit\Framework\TestCase
*/
protected $_requestMock;
+ /**
+ * @var Authorization|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $_authorizationMock;
+
/**
* @var VisibilityChecker|\PHPUnit_Framework_MockObject_MockObject
*/
@@ -86,11 +92,17 @@ protected function setUp()
$this->visibilityCheckerMock = $this->getMockBuilder(VisibilityChecker::class)
->getMockForAbstractClass();
+ $this->_authorizationMock = $this->getMockBuilder(Authorization::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isAllowed'])
+ ->getMock();
+
$arguments = [
'layout' => $this->_layoutMock,
'request' => $this->_requestMock,
'urlBuilder' => $this->_urlModelMock,
- 'data' => ['massaction_id_field' => 'test_id', 'massaction_id_filter' => 'test_id']
+ 'data' => ['massaction_id_field' => 'test_id', 'massaction_id_filter' => 'test_id'],
+ 'authorization' => $this->_authorizationMock,
];
$objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
@@ -145,6 +157,10 @@ public function testItemsProcessing($itemId, $item, $expectedItem)
->method('getUrl')
->willReturnMap($urlReturnValueMap);
+ $this->_authorizationMock->expects($this->any())
+ ->method('isAllowed')
+ ->willReturn(true);
+
$this->_block->addItem($itemId, $item);
$this->assertEquals(1, $this->_block->getCount());
@@ -184,6 +200,28 @@ public function itemsProcessingDataProvider()
"id" => 'test_id2',
]
)
+ ],
+ [
+ 'enabled',
+ new \Magento\Framework\DataObject(["label" => "Test Item Enabled", "url" => "*/*/test2"]),
+ new \Magento\Framework\DataObject(
+ [
+ "label" => "Test Item Enabled",
+ "url" => "http://localhost/index.php/backend/admin/test/test2",
+ "id" => 'enabled',
+ ]
+ )
+ ],
+ [
+ 'refresh',
+ new \Magento\Framework\DataObject(["label" => "Test Item Refresh", "url" => "*/*/test2"]),
+ new \Magento\Framework\DataObject(
+ [
+ "label" => "Test Item Refresh",
+ "url" => "http://localhost/index.php/backend/admin/test/test2",
+ "id" => 'refresh',
+ ]
+ )
]
];
}
@@ -237,7 +275,7 @@ public function testGetGridIdsJsonWithoutUseSelectAll()
public function testGetGridIdsJsonWithUseSelectAll(array $items, $result)
{
$this->_block->setUseSelectAll(true);
-
+
if ($this->_block->getMassactionIdField()) {
$massActionIdField = $this->_block->getMassactionIdField();
} else {
@@ -290,14 +328,20 @@ public function dataProviderGetGridIdsJsonWithUseSelectAll()
* @param int $count
* @param bool $withVisibilityChecker
* @param bool $isVisible
+ * @param bool $isAllowed
+ *
* @dataProvider addItemDataProvider
*/
- public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isVisible)
+ public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isVisible, $isAllowed)
{
$this->visibilityCheckerMock->expects($this->any())
->method('isVisible')
->willReturn($isVisible);
+ $this->_authorizationMock->expects($this->any())
+ ->method('isAllowed')
+ ->willReturn($isAllowed);
+
if ($withVisibilityChecker) {
$item['visible'] = $this->visibilityCheckerMock;
}
@@ -311,7 +355,7 @@ public function testAddItem($itemId, $item, $count, $withVisibilityChecker, $isV
->willReturnMap($urlReturnValueMap);
$this->_block->addItem($itemId, $item);
- $this->assertEquals($count, $this->_block->getCount());
+ $this->assertEquals($count, $this->_block->getCount(), $itemId);
}
/**
@@ -325,7 +369,8 @@ public function addItemDataProvider()
'item' => ['label' => 'Test 1', 'url' => '*/*/test1'],
'count' => 1,
'withVisibilityChecker' => false,
- '$isVisible' => false,
+ 'isVisible' => false,
+ 'isAllowed' => true,
],
[
'itemId' => 'test2',
@@ -333,21 +378,56 @@ public function addItemDataProvider()
'count' => 1,
'withVisibilityChecker' => false,
'isVisible' => true,
+ 'isAllowed' => true,
],
[
- 'itemId' => 'test1',
- 'item' => ['label' => 'Test 1. Hide', 'url' => '*/*/test1'],
+ 'itemId' => 'test3',
+ 'item' => ['label' => 'Test 3. Hide', 'url' => '*/*/test3'],
'count' => 0,
'withVisibilityChecker' => true,
'isVisible' => false,
+ 'isAllowed' => true,
],
[
- 'itemId' => 'test2',
- 'item' => ['label' => 'Test 2. Does not hide', 'url' => '*/*/test2'],
+ 'itemId' => 'test4',
+ 'item' => ['label' => 'Test 4. Does not hide', 'url' => '*/*/test4'],
'count' => 1,
'withVisibilityChecker' => true,
'isVisible' => true,
- ]
+ 'isAllowed' => true,
+ ],
+ [
+ 'itemId' => 'enable',
+ 'item' => ['label' => 'Test 5. Not restricted', 'url' => '*/*/test5'],
+ 'count' => 1,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => true,
+ ],
+ [
+ 'itemId' => 'enable',
+ 'item' => ['label' => 'Test 5. restricted', 'url' => '*/*/test5'],
+ 'count' => 0,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => false,
+ ],
+ [
+ 'itemId' => 'refresh',
+ 'item' => ['label' => 'Test 6. Not Restricted', 'url' => '*/*/test6'],
+ 'count' => 1,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => true,
+ ],
+ [
+ 'itemId' => 'refresh',
+ 'item' => ['label' => 'Test 6. Restricted', 'url' => '*/*/test6'],
+ 'count' => 0,
+ 'withVisibilityChecker' => true,
+ 'isVisible' => true,
+ 'isAllowed' => false,
+ ],
];
}
}
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php b/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php
index 31a13191750a3..2f0102ffd410d 100755
--- a/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Config/SessionLifetime/BackendModelTest.php
@@ -20,7 +20,8 @@ public function testBeforeSave($value, $errorMessage = null)
\Magento\Backend\Model\Config\SessionLifetime\BackendModel::class
);
if ($errorMessage !== null) {
- $this->expectException(\Magento\Framework\Exception\LocalizedException::class, $errorMessage);
+ $this->expectException(\Magento\Framework\Exception\LocalizedException::class);
+ $this->expectExceptionMessage($errorMessage);
}
$model->setValue($value);
$object = $model->beforeSave();
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php
index 74368537c39c7..ad172cbfbd165 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/ItemTest.php
@@ -56,9 +56,9 @@ class ItemTest extends \PHPUnit\Framework\TestCase
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
];
protected function setUp()
diff --git a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php
index b0c74461980a2..82f07e264b963 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_constructor_data.php
@@ -12,21 +12,21 @@
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
[
'parent_id' => null,
- 'module_name' => 'Magento_Backend',
+ 'module' => 'Magento_Backend',
'sort_index' => null,
- 'depends_on_config' => 'system/config/isEnabled',
+ 'dependsOnConfig' => 'system/config/isEnabled',
'id' => 'item',
'resource' => 'Magento_Config::config',
'path' => '',
'action' => '/system/config',
- 'depends_on_module' => 'Magento_Backend',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'toolTip' => 'Item tooltip',
'title' => 'Item Title',
'sub_menu' => null,
'target' => null
@@ -38,43 +38,43 @@
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => null,
+ 'dependsOnModule' => null,
+ 'toolTip' => null,
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => '',
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => ['submenuArray'],
'target' => null
@@ -83,51 +83,51 @@
'data with submenu to constructor' => [
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => null,
+ 'dependsOnModule' => null,
+ 'toolTip' => null,
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => null,
'resource' => null,
'path' => '',
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => ['submenuArray'],
'target' => null
diff --git a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php
index 30a43b0158ae3..b1a310d7d440b 100644
--- a/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php
+++ b/app/code/Magento/Backend/Test/Unit/Model/_files/menu_item_data.php
@@ -11,22 +11,22 @@
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
'sub_menu' => null,
],
[
'parent_id' => null,
- 'module_name' => 'Magento_Backend',
+ 'module' => 'Magento_Backend',
'sort_index' => null,
- 'depends_on_config' => 'system/config/isEnabled',
+ 'dependsOnConfig' => 'system/config/isEnabled',
'id' => 'item',
'resource' => 'Magento_Config::config',
'path' => '',
'action' => '/system/config',
- 'depends_on_module' => 'Magento_Backend',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'toolTip' => 'Item tooltip',
'title' => 'Item Title',
'sub_menu' => null,
'target' => null
@@ -35,46 +35,46 @@
'with submenu' => [
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => null,
+ 'dependsOnModule' => null,
+ 'toolTip' => null,
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => '5',
'resource' => null,
'path' => null,
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
'target' => null
]
@@ -82,38 +82,38 @@
'small set of data' => [
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
],
[
'parent_id' => '1',
- 'module_name' => 'Magento_Module1',
+ 'module' => 'Magento_Module1',
'sort_index' => '50',
- 'depends_on_config' => null,
+ 'dependsOnConfig' => null,
'id' => null,
'resource' => null,
'path' => '',
'action' => null,
- 'depends_on_module' => null,
- 'tooltip' => '',
+ 'dependsOnModule' => null,
+ 'toolTip' => '',
'title' => null,
'sub_menu' => [
'id' => 'item',
'title' => 'Item Title',
'action' => '/system/config',
'resource' => 'Magento_Config::config',
- 'depends_on_module' => 'Magento_Backend',
- 'depends_on_config' => 'system/config/isEnabled',
- 'tooltip' => 'Item tooltip',
+ 'dependsOnModule' => 'Magento_Backend',
+ 'dependsOnConfig' => 'system/config/isEnabled',
+ 'toolTip' => 'Item tooltip',
],
'target' => null
]
diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json
index 036f0b4d54a46..8d8be79f1c916 100644
--- a/app/code/Magento/Backend/composer.json
+++ b/app/code/Magento/Backend/composer.json
@@ -2,7 +2,7 @@
"name": "magento/module-backend",
"description": "N/A",
"require": {
- "php": "7.0.2|7.0.4|~7.0.6|~7.1.0",
+ "php": "~7.0.13|~7.1.0",
"magento/module-store": "100.2.*",
"magento/module-directory": "100.2.*",
"magento/module-developer": "100.2.*",
@@ -24,7 +24,7 @@
"magento/module-theme": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.0",
+ "version": "100.2.3",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Backend/etc/acl.xml b/app/code/Magento/Backend/etc/acl.xml
index af4ab5856e94c..cf9471e75bed9 100644
--- a/app/code/Magento/Backend/etc/acl.xml
+++ b/app/code/Magento/Backend/etc/acl.xml
@@ -38,7 +38,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/etc/adminhtml/di.xml b/app/code/Magento/Backend/etc/adminhtml/di.xml
index 97a39139411c0..de8286e7f3ecc 100644
--- a/app/code/Magento/Backend/etc/adminhtml/di.xml
+++ b/app/code/Magento/Backend/etc/adminhtml/di.xml
@@ -145,6 +145,9 @@
- Magento\Config\Model\Config\Structure\ElementVisibilityInterface::HIDDEN
- Magento\Config\Model\Config\Structure\ElementVisibilityInterface::DISABLED
+
+
+
@@ -152,4 +155,20 @@
Magento\Backend\Block\Template
+
+
+
+ - Magento_Catalog::products
+ - Magento_Sales::sales_order
+ - Magento_Customer::manage
+ - Magento_Cms::page
+
+
+ - catalog/product/index/
+ - sales/order/index/
+ - customer/index/index
+ - cms/page/index/
+
+
+
diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml
index 27fd16cc920dc..3a58ef9f63e8d 100644
--- a/app/code/Magento/Backend/etc/adminhtml/system.xml
+++ b/app/code/Magento/Backend/etc/adminhtml/system.xml
@@ -215,7 +215,7 @@
European Union Countries
Magento\Directory\Model\Config\Source\Country
-
+
Top destinations
Magento\Directory\Model\Config\Source\Country
@@ -409,7 +409,7 @@
Web
general
Magento_Config::web
-
+
Url Options
Add Store Code to Urls
@@ -419,7 +419,7 @@
Warning! When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third party services (e.g. PayPal etc.).]]>
-
+
Auto-redirect to Base URL
Magento\Config\Model\Config\Source\Web\Redirect
I.e. redirect from http://example.com/store/ to http://www.example.com/store/
diff --git a/app/code/Magento/Backend/i18n/en_US.csv b/app/code/Magento/Backend/i18n/en_US.csv
index 2730d4d92835b..aa28a670b9205 100644
--- a/app/code/Magento/Backend/i18n/en_US.csv
+++ b/app/code/Magento/Backend/i18n/en_US.csv
@@ -214,10 +214,13 @@ YTD,YTD
"Admin session lifetime must be greater than or equal to 60 seconds","Admin session lifetime must be greater than or equal to 60 seconds"
Order,Order
"Order #%1","Order #%1"
-"Access denied","Access denied"
-"Please try to sign out and sign in again.","Please try to sign out and sign in again."
-"If you continue to receive this message, please contact the store owner.","If you continue to receive this message, please contact the store owner."
"You need more permissions to access this.","You need more permissions to access this."
+"Sorry, you need permissions to view this content.","Sorry, you need permissions to view this content."
+"Next steps","Next steps"
+"If you think this is an error, try signing out and signing in again.","If you think this is an error, try signing out and signing in again."
+"Contact a system administrator or store owner to gain permissions.","Contact a system administrator or store owner to gain permissions."
+"Return to","Return to"
+"previous page","previous page"
"Welcome, please sign in","Welcome, please sign in"
Username,Username
"user name","user name"
@@ -458,3 +461,7 @@ Pagination,Pagination
"Alternative text for the next pages link in the pagination menu. If empty, default arrow image is used.","Alternative text for the next pages link in the pagination menu. If empty, default arrow image is used."
"Anchor Text for Next","Anchor Text for Next"
"Theme Name","Theme Name"
+"In Products","In Products"
+"In Orders","In Orders"
+"In Customers","In Customers"
+"In Pages","In Pages"
diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml
index ab5ddc414b51f..4bbe70b6cdb92 100644
--- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml
+++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_index.xml
@@ -11,7 +11,11 @@
-
+
+
+ Magento\Backend\Block\Cache\Permissions
+
+
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
index 852ecd5a07962..843328fbf17d7 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/access_denied.phtml
@@ -12,12 +12,23 @@
* @see \Magento\Backend\Block\Denied
*/
?>
-= /* @escapeNotVerified */ __('Access denied') ?>
-hasAvailableResources()): ?>
-
-= /* @escapeNotVerified */ __('Please try to sign out and sign in again.') ?>
-= /* @escapeNotVerified */ __('If you continue to receive this message, please contact the store owner.') ?>
-
-
-= /* @escapeNotVerified */ __('You need more permissions to access this.') ?>
-
+
+
+
= $block->escapeHtml(__('Sorry, you need permissions to view this content.')) ?>
+
= $block->escapeHtml(__('Next steps')) ?>
+
+
diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml
index 40b7173f47417..8feccc9cf1b8f 100644
--- a/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml
+++ b/app/code/Magento/Backend/view/adminhtml/templates/page/header.phtml
@@ -29,7 +29,7 @@
data-mage-init='{"dropdown":{}}'
data-toggle="dropdown">
- = $block->escapeHtml($block->getUser()->getUsername()) ?>
+ = $block->escapeHtml($block->getUser()->getUserName()) ?>