diff --git a/.gitignore b/.gitignore index 75e5f11d8a8e7..a79b7990a7576 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ atlassian* /pub/media/import/* !/pub/media/import/.htaccess /pub/media/logo/* +/pub/media/custom_options/* +!/pub/media/custom_options/.htaccess /pub/media/theme/* /pub/media/theme_customization/* !/pub/media/theme_customization/.htaccess diff --git a/.htaccess b/.htaccess index 4298b10d9ca7a..cc59be5480798 100644 --- a/.htaccess +++ b/.htaccess @@ -29,6 +29,8 @@ ############################################ ## default index file +## Specifies option, to use methods arguments in backtrace or not + SetEnv MAGE_DEBUG_SHOW_ARGS 1 DirectoryIndex index.php @@ -364,6 +366,15 @@ Require all denied + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + # For 404s and 403s that aren't handled by the application, show plain 404 response ErrorDocument 404 /pub/errors/404.php diff --git a/.htaccess.sample b/.htaccess.sample index a521a347232f5..b405fd3a22b75 100644 --- a/.htaccess.sample +++ b/.htaccess.sample @@ -341,6 +341,15 @@ Require all denied + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + # For 404s and 403s that aren't handled by the application, show plain 404 response ErrorDocument 404 /pub/errors/404.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9ca068019dc..4fb34dd58c46d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,476 @@ +2.2.8 +============= +* GitHub issues: + * [#15196](https://github.com/magento/magento2/issues/15196) -- 2.2.4 : Magento 2 integration tests enables all modules (fixed in [magento/magento2#16361](https://github.com/magento/magento2/pull/16361)) + * [#13720](https://github.com/magento/magento2/issues/13720) -- Only 2 related products are showing in backend . (fixed in [magento/magento2#17885](https://github.com/magento/magento2/pull/17885)) + * [#14050](https://github.com/magento/magento2/issues/14050) -- Import related products issue (fixed in [magento/magento2#17885](https://github.com/magento/magento2/pull/17885)) + * [#17890](https://github.com/magento/magento2/issues/17890) -- Magento 2.2.5 Product swatches does not shows correct value for related store view (fixed in [magento/magento2#17891](https://github.com/magento/magento2/pull/17891)) + * [#17567](https://github.com/magento/magento2/issues/17567) -- Currency symbol cannot be changed back to default value from admin panel in Single-store mode (fixed in [magento/magento2#17966](https://github.com/magento/magento2/pull/17966)) + * [#5402](https://github.com/magento/magento2/issues/5402) -- Menu does not work when you change from Mobile to Desktop mode (fixed in [magento/magento2#17990](https://github.com/magento/magento2/pull/17990)) + * [#13405](https://github.com/magento/magento2/issues/13405) -- No such entity error when saving product in single-store mode if website_id <> 1 (fixed in [magento/magento2#18001](https://github.com/magento/magento2/pull/18001)) + * [#5797](https://github.com/magento/magento2/issues/5797) -- [2.1.0] module:uninstall can remove code it uses itself (fixed in [magento/magento2#18002](https://github.com/magento/magento2/pull/18002)) + * [#17780](https://github.com/magento/magento2/issues/17780) -- Module uninstall does not work with composer (fixed in [magento/magento2#18002](https://github.com/magento/magento2/pull/18002)) + * [#7557](https://github.com/magento/magento2/issues/7557) -- Backend Security key broken for controllers with frontname not equal to route ID (fixed in [magento/magento2#18018](https://github.com/magento/magento2/pull/18018)) + * [#12095](https://github.com/magento/magento2/issues/12095) -- Update 2.2.1: One or more integrations have been reset because of a change to their xml configs. (fixed in [magento/magento2#14065](https://github.com/magento/magento2/pull/14065)) + * [#17582](https://github.com/magento/magento2/issues/17582) -- ./bin/magento config:show fails with a fatal error (fixed in [magento/magento2#17993](https://github.com/magento/magento2/pull/17993)) + * [#17999](https://github.com/magento/magento2/issues/17999) -- Sitemap grid display incorrect base URL in the grid if using multiple stores (fixed in [magento/magento2#18000](https://github.com/magento/magento2/pull/18000)) + * [#9830](https://github.com/magento/magento2/issues/9830) -- Null order in Magento\Sales\Block\Order\PrintShipment.php (fixed in [magento/magento2#17998](https://github.com/magento/magento2/pull/17998)) + * [#10530](https://github.com/magento/magento2/issues/10530) -- Print order error on magento 2.1.8 (fixed in [magento/magento2#17998](https://github.com/magento/magento2/pull/17998)) + * [#10440](https://github.com/magento/magento2/issues/10440) -- Missing $debugHintsPath when sending email via command (fixed in [magento/magento2#17984](https://github.com/magento/magento2/pull/17984)) + * [#18079](https://github.com/magento/magento2/issues/18079) -- Inconsistent return type for getStoreId() (fixed in [magento/magento2#18086](https://github.com/magento/magento2/pull/18086)) + * [#18138](https://github.com/magento/magento2/issues/18138) -- WYSIWYG editor fails to parse directives of files with special characters in URL (so random files) (fixed in [magento/magento2#18215](https://github.com/magento/magento2/pull/18215)) + * [#18101](https://github.com/magento/magento2/issues/18101) -- Wrong sort order for customer groups in customer grid filter (fixed in [magento/magento2#18280](https://github.com/magento/magento2/pull/18280)) + * [#17977](https://github.com/magento/magento2/issues/17977) -- Show Method if Not Applicable for Free Shipping doesn't work. (fixed in [magento/magento2#17982](https://github.com/magento/magento2/pull/17982)) + * [#17023](https://github.com/magento/magento2/issues/17023) -- CSV Import of `sku,attribute` empties `url_key` value (fixed in [magento/magento2#17882](https://github.com/magento/magento2/pull/17882)) + * [#18330](https://github.com/magento/magento2/issues/18330) -- Checkout - Infinite loading indicator when server returned error (fixed in [magento/magento2#18369](https://github.com/magento/magento2/pull/18369)) + * [#16497](https://github.com/magento/magento2/issues/16497) -- Magento 2.2.5: Google Analytics not added to head correctly (fixed in [magento/magento2#18375](https://github.com/magento/magento2/pull/18375)) + * [#17152](https://github.com/magento/magento2/issues/17152) -- Failure of "Send Order Email Copy" spams customers, every minute, forever. (fixed in [magento/magento2#18376](https://github.com/magento/magento2/pull/18376)) + * [#18162](https://github.com/magento/magento2/issues/18162) -- Cannot edit customer using inline edit if password is expired (fixed in [magento/magento2#18414](https://github.com/magento/magento2/pull/18414)) + * [#3283](https://github.com/magento/magento2/issues/3283) -- «Yes/No» attributes should be allowed in the Layered Navigation (fixed in [magento/magento2#17823](https://github.com/magento/magento2/pull/17823)) + * [#17493](https://github.com/magento/magento2/issues/17493) -- Catalog Rule & Selected Categories with level > 3 (fixed in [magento/magento2#18175](https://github.com/magento/magento2/pull/18175)) + * [#17770](https://github.com/magento/magento2/issues/17770) -- Table rate fail when using ZIP+4 shipping address (fixed in [magento/magento2#18166](https://github.com/magento/magento2/pull/18166)) + * [#13156](https://github.com/magento/magento2/issues/13156) -- Updating attribute option data through API will set unwanted source_model on the attribute (fixed in [magento/magento2#18390](https://github.com/magento/magento2/pull/18390)) + * [#17190](https://github.com/magento/magento2/issues/17190) -- system.log rapidly increasing after Magento CE 2.2.5 update (cron logs) (fixed in [magento/magento2#18389](https://github.com/magento/magento2/pull/18389)) + * [#15085](https://github.com/magento/magento2/issues/15085) -- StockRegistryInterface :: getLowStockItems() returns StockStatusCollection instead of StockItemCollection (fixed in [magento/magento2#18427](https://github.com/magento/magento2/pull/18427)) + * [#15652](https://github.com/magento/magento2/issues/15652) -- REST API create order POST /V1/orders (fixed in [magento/magento2#15683](https://github.com/magento/magento2/pull/15683)) + * [#4942](https://github.com/magento/magento2/issues/4942) -- On editing a Bundle product from shopping cart the user defined quantities of the options are overwritten (fixed in [magento/magento2#15905](https://github.com/magento/magento2/pull/15905)) + * [#17514](https://github.com/magento/magento2/issues/17514) -- Add Australian regions (fixed in [magento/magento2#17516](https://github.com/magento/magento2/pull/17516)) + * [#12479](https://github.com/magento/magento2/issues/12479) -- Saving Customer Model directly causes loss of data (fixed in [magento/magento2#17968](https://github.com/magento/magento2/pull/17968)) + * [#9219](https://github.com/magento/magento2/issues/9219) -- Custom Product Attribute changes 'backend_type' when 'is_user_defined = 1' and get updated/saved in Admin Backend (fixed in [magento/magento2#18196](https://github.com/magento/magento2/pull/18196)) + * [#18164](https://github.com/magento/magento2/issues/18164) -- Checkout - Cannot read property 'code' of undefined (fixed in [magento/magento2#18495](https://github.com/magento/magento2/pull/18495)) + * [#14555](https://github.com/magento/magento2/issues/14555) -- Communication's component validator does not propagate exceptions, obscuring the cause of the error (fixed in [magento/magento2#18554](https://github.com/magento/magento2/pull/18554)) + * [#18477](https://github.com/magento/magento2/issues/18477) -- Set maximum Qty Allowed in Shopping Cart is 0 still allow adding to cart (fixed in [magento/magento2#18552](https://github.com/magento/magento2/pull/18552)) + * [#12070](https://github.com/magento/magento2/issues/12070) -- M2.2.0 Admin Grid column ordering/positioning not working when single store mode set On (fixed in [magento/magento2#18561](https://github.com/magento/magento2/pull/18561)) + * [#18581](https://github.com/magento/magento2/issues/18581) -- Calendar Icon aligement Issue (fixed in [magento/magento2#18593](https://github.com/magento/magento2/pull/18593)) + * [#18585](https://github.com/magento/magento2/issues/18585) -- Navigation arrows zoomed fotorama disappear (fixed in [magento/magento2#18595](https://github.com/magento/magento2/pull/18595)) + * [#12969](https://github.com/magento/magento2/issues/12969) -- processor.php getHostUrl() does not detect the server port correctly (fixed in [magento/magento2#18659](https://github.com/magento/magento2/pull/18659)) + * [#14510](https://github.com/magento/magento2/issues/14510) -- Creating custom customer attribute with default value 0 will cause not saving value for customer entity (fixed in [magento/magento2#16915](https://github.com/magento/magento2/pull/16915)) + * [#18234](https://github.com/magento/magento2/issues/18234) -- Product Import -> Upsert Category: Url Rewrites are just created for default website (fixed in [magento/magento2#18563](https://github.com/magento/magento2/pull/18563)) + * [#5929](https://github.com/magento/magento2/issues/5929) -- Saving Product does not update URL rewrite in Magento 2.1.0 (fixed in [magento/magento2#18566](https://github.com/magento/magento2/pull/18566)) + * [#18532](https://github.com/magento/magento2/issues/18532) -- Module Catalog: product "Save and Duplicate" causes getting infinite loop (fixed in [magento/magento2#18566](https://github.com/magento/magento2/pull/18566)) + * [#18131](https://github.com/magento/magento2/issues/18131) -- Entity Type ID at Join (fixed in [magento/magento2#18658](https://github.com/magento/magento2/pull/18658)) + * [#15259](https://github.com/magento/magento2/issues/15259) -- Advanced Reporting > Unable to disable without providing Industry value (fixed in [magento/magento2#15366](https://github.com/magento/magento2/pull/15366)) + * [#18094](https://github.com/magento/magento2/issues/18094) -- Should getQty() return int/float or string? (fixed in [magento/magento2#18424](https://github.com/magento/magento2/pull/18424)) + * [#18534](https://github.com/magento/magento2/issues/18534) -- Bug when 2 wysiwyg editors are on category edit page or product edit page (fixed in [magento/magento2#18535](https://github.com/magento/magento2/pull/18535)) + * [#18589](https://github.com/magento/magento2/issues/18589) -- Empty cart button does not work (fixed in [magento/magento2#18597](https://github.com/magento/magento2/pull/18597)) + * [#18268](https://github.com/magento/magento2/issues/18268) -- M2.2.6 : Special price of 0.0000 is not shown on frontend, but is calculated in cart (fixed in [magento/magento2#18604](https://github.com/magento/magento2/pull/18604)) + * [#17954](https://github.com/magento/magento2/issues/17954) -- Customer get unsubscribe to newsletter on password reset email request with Newsletter Need to Confirm Set to Yes on admin settings (fixed in [magento/magento2#18643](https://github.com/magento/magento2/pull/18643)) + * [#16939](https://github.com/magento/magento2/issues/16939) -- Incorrect configuration scope is occasionally returned when attempting to resolve a null scope id (fixed in [magento/magento2#16940](https://github.com/magento/magento2/pull/16940)) + * [#18264](https://github.com/magento/magento2/issues/18264) -- M2.2.6 : "Order by price" not working in product listing (fixed in [magento/magento2#18737](https://github.com/magento/magento2/pull/18737)) + * [#17638](https://github.com/magento/magento2/issues/17638) -- Bundle Special Prices not correctly rounded (fixed in [magento/magento2#17971](https://github.com/magento/magento2/pull/17971)) + * [#17865](https://github.com/magento/magento2/issues/17865) -- import new products via csv: products are created with empty value when strings are too long (fixed in [magento/magento2#18591](https://github.com/magento/magento2/pull/18591)) + * [#12300](https://github.com/magento/magento2/issues/12300) -- SKU values are not trimmed with the space. (fixed in [magento/magento2#18862](https://github.com/magento/magento2/pull/18862)) + * [#16572](https://github.com/magento/magento2/issues/16572) -- Trim whitespace on SKU when saving product (fixed in [magento/magento2#18862](https://github.com/magento/magento2/pull/18862)) + * [#18458](https://github.com/magento/magento2/issues/18458) -- Magento version 2.2.6 Alert widget gets close when click anywhere on screen (fixed in [magento/magento2#18865](https://github.com/magento/magento2/pull/18865)) + * [#18779](https://github.com/magento/magento2/issues/18779) -- Translation issue send-friend in sendphtml (fixed in [magento/magento2#18886](https://github.com/magento/magento2/pull/18886)) + * [#18913](https://github.com/magento/magento2/issues/18913) -- Global-search icon misaligned (fixed in [magento/magento2#18917](https://github.com/magento/magento2/pull/18917)) + * [#17488](https://github.com/magento/magento2/issues/17488) -- Authenticating a customer via REST API does not update the last logged in data (fixed in [magento/magento2#17978](https://github.com/magento/magento2/pull/17978)) + * [#4468](https://github.com/magento/magento2/issues/4468) -- Unable to insert multiple catalog product list widgets in CMS page (fixed in [magento/magento2#18874](https://github.com/magento/magento2/pull/18874)) + * [#18355](https://github.com/magento/magento2/issues/18355) -- Typo in dispatched event name (fixed in [magento/magento2#18372](https://github.com/magento/magento2/pull/18372)) + * [#17744](https://github.com/magento/magento2/issues/17744) -- Virtual-only quotes use default shipping address for estimation instead of default billing address (fixed in [magento/magento2#18863](https://github.com/magento/magento2/pull/18863)) + * [#5021](https://github.com/magento/magento2/issues/5021) -- "Please specify a shipping method" Exception (fixed in [magento/magento2#18870](https://github.com/magento/magento2/pull/18870)) + * [#17485](https://github.com/magento/magento2/issues/17485) -- Adding billing information via mine API expects costumer id (fixed in [magento/magento2#18872](https://github.com/magento/magento2/pull/18872)) + * [#13083](https://github.com/magento/magento2/issues/13083) -- OptionManagement.validateOption throws NoSuchEntityException for "0" option label (fixed in [magento/magento2#18873](https://github.com/magento/magento2/pull/18873)) + * [#18729](https://github.com/magento/magento2/issues/18729) -- Bug in "_sections.less" mixins: missing rules and incorrect default variables (fixed in [magento/magento2#18875](https://github.com/magento/magento2/pull/18875)) + * [#18555](https://github.com/magento/magento2/issues/18555) -- Magento 2.2.6 Default values are not rendering on Wishlist product edit page. (fixed in [magento/magento2#18967](https://github.com/magento/magento2/pull/18967)) + * [#18907](https://github.com/magento/magento2/issues/18907) -- Unable to select payment method according to country of the address at checkout time (fixed in [magento/magento2#18908](https://github.com/magento/magento2/pull/18908)) + * [#16684](https://github.com/magento/magento2/issues/16684) -- Default tax region/state appears in customer & order data (fixed in [magento/magento2#18857](https://github.com/magento/magento2/pull/18857)) + * [#8348](https://github.com/magento/magento2/issues/8348) -- 1 exception(s): Exception #0 (Exception): Warning: Invalid argument supplied for foreach() in NotProtectedExtension.php on line 89 (fixed in [magento/magento2#19012](https://github.com/magento/magento2/pull/19012)) + * [#18323](https://github.com/magento/magento2/issues/18323) -- Order confirmation email for guest checkout does not include download links (fixed in [magento/magento2#19036](https://github.com/magento/magento2/pull/19036)) + * [#19003](https://github.com/magento/magento2/issues/19003) -- salesInvoiceOrder REST API does not make downloadable products available (fixed in [magento/magento2#19036](https://github.com/magento/magento2/pull/19036)) + * [#19034](https://github.com/magento/magento2/issues/19034) -- sales_order_item_save_commit_after and sales_order_save_commit_after events will never fire for guest checkout (fixed in [magento/magento2#19036](https://github.com/magento/magento2/pull/19036)) + * [#2618](https://github.com/magento/magento2/issues/2618) -- Class \Magento\Framework\Data\Form\Element\Fieldset breaks specification of the parent class \Magento\Framework\Data\Form\Element\AbstractElement by not calling the method getBeforeElementHtml (getAfterElementHtml is called) (fixed in [magento/magento2#18985](https://github.com/magento/magento2/pull/18985)) + * [#14007](https://github.com/magento/magento2/issues/14007) -- "Use in Layered Navigation: Filterable (no results)" not working for Price attribute. (fixed in [magento/magento2#19044](https://github.com/magento/magento2/pull/19044)) + * [#12399](https://github.com/magento/magento2/issues/12399) -- Exception Error in Catalog Price Rule while Backend language is not English (fixed in [magento/magento2#19074](https://github.com/magento/magento2/pull/19074)) + * [#18082](https://github.com/magento/magento2/issues/18082) -- Fatal Error when save configurable product in Magento 2.2.5 (fixed in [magento/magento2#18461](https://github.com/magento/magento2/pull/18461)) + * [#18617](https://github.com/magento/magento2/issues/18617) -- Missing Fixed Product Tax total on PDF (fixed in [magento/magento2#18649](https://github.com/magento/magento2/pull/18649)) + * [#18150](https://github.com/magento/magento2/issues/18150) -- Backups error from User Roles Permission 2.2.6 (fixed in [magento/magento2#18815](https://github.com/magento/magento2/pull/18815)) + * [#18901](https://github.com/magento/magento2/issues/18901) -- Forgot password form should not available while customer is logged in. (fixed in [magento/magento2#19089](https://github.com/magento/magento2/pull/19089)) + * [#18840](https://github.com/magento/magento2/issues/18840) -- Invalid Unit Test Annotations (fixed in [magento/magento2#19105](https://github.com/magento/magento2/pull/19105)) + * [#19060](https://github.com/magento/magento2/issues/19060) -- User created by admin cannot login (fixed in [magento/magento2#19110](https://github.com/magento/magento2/pull/19110)) + * [#14849](https://github.com/magento/magento2/issues/14849) -- In Sales Emails no translation using order.getStatusLabel() (fixed in [magento/magento2#14914](https://github.com/magento/magento2/pull/14914)) + * [#17625](https://github.com/magento/magento2/issues/17625) -- Translations done within a theme that's enabled through a category Design change aren't used (fixed in [magento/magento2#17854](https://github.com/magento/magento2/pull/17854)) + * [#17635](https://github.com/magento/magento2/issues/17635) -- addExpressionFieldToSelect has to be called after all addFieldToSelect (fixed in [magento/magento2#17915](https://github.com/magento/magento2/pull/17915)) + * [#18652](https://github.com/magento/magento2/issues/18652) -- Tierprice discount not calculated correctly if has specialprice. (fixed in [magento/magento2#18743](https://github.com/magento/magento2/pull/18743)) + * [#18939](https://github.com/magento/magento2/issues/18939) -- "Not yet calculated" for the tax in the summary section in the checkout is not translatable (fixed in [magento/magento2#18959](https://github.com/magento/magento2/pull/18959)) + * [#16434](https://github.com/magento/magento2/issues/16434) -- Bundle Product Options not showing in Customer Account - Items Ordered (fixed in [magento/magento2#17889](https://github.com/magento/magento2/pull/17889)) + * [#14020](https://github.com/magento/magento2/issues/14020) -- Cart Sales Rule with negated condition over special_price does not work for configurable products (fixed in [magento/magento2#16342](https://github.com/magento/magento2/pull/16342)) + * [#18685](https://github.com/magento/magento2/issues/18685) -- Quote Item Prices are NULL in cart related events. (fixed in [magento/magento2#18808](https://github.com/magento/magento2/pull/18808)) + * [#18956](https://github.com/magento/magento2/issues/18956) -- Import of RootCategoryId should be possbile (Magento/Store/Model/Config/Importer/Processor/Create.php) (fixed in [magento/magento2#19237](https://github.com/magento/magento2/pull/19237)) + * [#19205](https://github.com/magento/magento2/issues/19205) -- Bundle Product Option with input type is checkbox and add to cart with 3 values only 2 values added to cart (fixed in [magento/magento2#19260](https://github.com/magento/magento2/pull/19260)) + * [#6803](https://github.com/magento/magento2/issues/6803) -- Product::addImageToMediaGallery throws Exception (fixed in [magento/magento2#18951](https://github.com/magento/magento2/pull/18951)) + * [#18949](https://github.com/magento/magento2/issues/18949) -- dev/tools/grunt/configs/themes.js gets replaced after update magento (fixed in [magento/magento2#18960](https://github.com/magento/magento2/pull/18960)) + * [#19054](https://github.com/magento/magento2/issues/19054) -- Using Media Image custom attribute type could not display on frontend. (fixed in [magento/magento2#19068](https://github.com/magento/magento2/pull/19068)) + * [#19082](https://github.com/magento/magento2/issues/19082) -- Fatal error: Uncaught Error: Cannot call abstract method Magento\Framework\App\ActionInterface::execute() (fixed in [magento/magento2#19337](https://github.com/magento/magento2/pull/19337)) + * [#19263](https://github.com/magento/magento2/issues/19263) -- Broken backend popup view (fixed in [magento/magento2#19340](https://github.com/magento/magento2/pull/19340)) + * [#4136](https://github.com/magento/magento2/issues/4136) -- Widget condition with unexpected character not preventing from saving (fixed in [magento/magento2#14485](https://github.com/magento/magento2/pull/14485)) + * [#18615](https://github.com/magento/magento2/issues/18615) -- Field restriction incompatibilities between klarna_core_order and sales_order_payment last_trans_id (fixed in [magento/magento2#18621](https://github.com/magento/magento2/pull/18621)) + * [#18904](https://github.com/magento/magento2/issues/18904) -- Missing asterisk for admin required fields (fixed in [magento/magento2#18905](https://github.com/magento/magento2/pull/18905)) + * [#19286](https://github.com/magento/magento2/issues/19286) -- Wrong pager style (fixed in [magento/magento2#19296](https://github.com/magento/magento2/pull/19296)) + * [#13157](https://github.com/magento/magento2/issues/13157) -- Last Ordered Items block - bad js code (fixed in [magento/magento2#19357](https://github.com/magento/magento2/pull/19357)) + * [#17833](https://github.com/magento/magento2/issues/17833) -- Child theme does not inherit translations from parent theme (fixed in [magento/magento2#19023](https://github.com/magento/magento2/pull/19023)) + * [#18839](https://github.com/magento/magento2/issues/18839) -- can't import external http to https redirecting images by default csv import (fixed in [magento/magento2#18899](https://github.com/magento/magento2/pull/18899)) + * [#18887](https://github.com/magento/magento2/issues/18887) -- Magento backend Notifications counter round icon small cut from right side (fixed in [magento/magento2#19356](https://github.com/magento/magento2/pull/19356)) + * [#17813](https://github.com/magento/magento2/issues/17813) -- Huge "product_data_storage" in localStorage hangs the shop (fixed in [magento/magento2#19014](https://github.com/magento/magento2/pull/19014)) + * [#15505](https://github.com/magento/magento2/issues/15505) -- Interceptor class methods do not support nullable return types (fixed in [magento/magento2#19398](https://github.com/magento/magento2/pull/19398)) + * [#19172](https://github.com/magento/magento2/issues/19172) -- Newsletter subscription does not set the correct store_id if already subscribed. Not Fixed in 2.3-dev (fixed in [magento/magento2#19426](https://github.com/magento/magento2/pull/19426)) + * [#18918](https://github.com/magento/magento2/issues/18918) -- Asterisk sign display twice (fixed in [magento/magento2#18922](https://github.com/magento/magento2/pull/18922)) + * [#19127](https://github.com/magento/magento2/issues/19127) -- Cannot connect to Magento 2 market place (fixed in [magento/magento2#19239](https://github.com/magento/magento2/pull/19239)) + * [#19344](https://github.com/magento/magento2/issues/19344) -- Sample Link Issue in Downloadable product. (fixed in [magento/magento2#19431](https://github.com/magento/magento2/pull/19431)) + * [#15931](https://github.com/magento/magento2/issues/15931) -- events.xml cant have no childrens, others can [Magento 2.2.4] (fixed in [magento/magento2#19145](https://github.com/magento/magento2/pull/19145)) + * [#19418](https://github.com/magento/magento2/issues/19418) -- Cannot add additional field to Newsletter system configuration at desired position (fixed in [magento/magento2#19568](https://github.com/magento/magento2/pull/19568)) + * [#19424](https://github.com/magento/magento2/issues/19424) -- \Magento\Checkout\Observer\SalesQuoteSaveAfterObserver fails to update the checkout session quote id when applicable (fixed in [magento/magento2#19678](https://github.com/magento/magento2/pull/19678)) + * [#19796](https://github.com/magento/magento2/issues/19796) -- Sales Order invoice Update Qty's Button is misaligned (fixed in [magento/magento2#19804](https://github.com/magento/magento2/pull/19804)) + * [#19917](https://github.com/magento/magento2/issues/19917) -- allowDrug? ;-) (fixed in [magento/magento2#19949](https://github.com/magento/magento2/pull/19949)) + * [#19721](https://github.com/magento/magento2/issues/19721) -- Typo in SalesRule/Model/ResourceModel/Coupon/Usage.php (fixed in [magento/magento2#19968](https://github.com/magento/magento2/pull/19968)) + * [#8952](https://github.com/magento/magento2/issues/8952) -- You can't subscribe to newsletter if you already have an account (fixed in [magento/magento2#18912](https://github.com/magento/magento2/pull/18912)) + * [#19142](https://github.com/magento/magento2/issues/19142) -- Home page store loge should be clickable to reload page (fixed in [magento/magento2#19199](https://github.com/magento/magento2/pull/19199)) + * [#18374](https://github.com/magento/magento2/issues/18374) -- Unable to get product attribute value for store-view scope type in product collection loaded for a specific store. (fixed in [magento/magento2#19911](https://github.com/magento/magento2/pull/19911)) + * [#18941](https://github.com/magento/magento2/issues/18941) -- Calling getCurrentUrl on Store will wrongly add "___store" parameter (fixed in [magento/magento2#19945](https://github.com/magento/magento2/pull/19945)) + * [#19052](https://github.com/magento/magento2/issues/19052) -- Position order showing before the text box (fixed in [magento/magento2#19056](https://github.com/magento/magento2/pull/19056)) + * [#19285](https://github.com/magento/magento2/issues/19285) -- On Notification page Select All and Select Visible both works same (fixed in [magento/magento2#19910](https://github.com/magento/magento2/pull/19910)) + * [#19507](https://github.com/magento/magento2/issues/19507) -- Frontend Minicart dropdown alignment issue (fixed in [magento/magento2#19889](https://github.com/magento/magento2/pull/19889)) + * [#19605](https://github.com/magento/magento2/issues/19605) -- Don't static compile disabled modules (fixed in [magento/magento2#19989](https://github.com/magento/magento2/pull/19989)) + * [#19346](https://github.com/magento/magento2/issues/19346) -- Import data 2.2.6 Value for 'product_type' attribute contains incorrect value (fixed in [magento/magento2#20081](https://github.com/magento/magento2/pull/20081)) + * [#19780](https://github.com/magento/magento2/issues/19780) -- Incorrect class name on Orders and returns page. (fixed in [magento/magento2#20080](https://github.com/magento/magento2/pull/20080)) + * [#19230](https://github.com/magento/magento2/issues/19230) -- Can't Cancel Order (fixed in [magento/magento2#19423](https://github.com/magento/magento2/pull/19423)) + * [#19099](https://github.com/magento/magento2/issues/19099) -- New Link is not correctly shown as Current if contains default parts (fixed in [magento/magento2#19927](https://github.com/magento/magento2/pull/19927)) + * [#19940](https://github.com/magento/magento2/issues/19940) -- Exception undefined variable itemsOrderItemId while creating shipment through MSI (fixed in [magento/magento2#20082](https://github.com/magento/magento2/pull/20082)) + * [#19101](https://github.com/magento/magento2/issues/19101) -- API REST and Reserved Order Id (fixed in [magento/magento2#20208](https://github.com/magento/magento2/pull/20208)) + * [#20210](https://github.com/magento/magento2/issues/20210) -- Hamburger Icon was available on a page where menu was not present. Issue in responsive view (fixed in [magento/magento2#20219](https://github.com/magento/magento2/pull/20219)) + * [#16198](https://github.com/magento/magento2/issues/16198) -- Category image remain after deleted (fixed in [magento/magento2#20178](https://github.com/magento/magento2/pull/20178)) + * [#18192](https://github.com/magento/magento2/issues/18192) -- Backend issue : "ratings isn't available" website wise (fixed in [magento/magento2#20183](https://github.com/magento/magento2/pull/20183)) + * [#14937](https://github.com/magento/magento2/issues/14937) -- Javascript error thrown from uiComponent 'notification_area' if messages are malformed (fixed in [magento/magento2#20271](https://github.com/magento/magento2/pull/20271)) + * [#17819](https://github.com/magento/magento2/issues/17819) -- Wrong product url from getProductUrl when current category has not product object (fixed in [magento/magento2#20286](https://github.com/magento/magento2/pull/20286)) + * [#20296](https://github.com/magento/magento2/issues/20296) -- "@magentoDataIsolation" is used instead of "@magentoDbIsolation" in some integration tests. (fixed in [magento/magento2#20298](https://github.com/magento/magento2/pull/20298)) + * [#20158](https://github.com/magento/magento2/issues/20158) -- Store switcher not aligned proper in tab view (fixed in [magento/magento2#20325](https://github.com/magento/magento2/pull/20325)) + * [#20232](https://github.com/magento/magento2/issues/20232) -- Backend order credit card detail check box misaligned (fixed in [magento/magento2#20328](https://github.com/magento/magento2/pull/20328)) + * [#20098](https://github.com/magento/magento2/issues/20098) -- Product image failure when importing through CSV (fixed in [magento/magento2#20329](https://github.com/magento/magento2/pull/20329)) + * [#20352](https://github.com/magento/magento2/issues/20352) -- File type option value shows html content in admin order view. (fixed in [magento/magento2#20353](https://github.com/magento/magento2/pull/20353)) + * [#18170](https://github.com/magento/magento2/issues/18170) -- Unable to reset password if customer has address from not allowed country (fixed in [magento/magento2#19964](https://github.com/magento/magento2/pull/19964)) + * [#19982](https://github.com/magento/magento2/issues/19982) -- Catalogsearch Reindex (fixed in [magento/magento2#19984](https://github.com/magento/magento2/pull/19984)) + * [#9130](https://github.com/magento/magento2/issues/9130) -- If stock is bellow OutOfStock Threshold, a negative qty is displayed in Product List Page (fixed in [magento/magento2#20206](https://github.com/magento/magento2/pull/20206)) + * [#19609](https://github.com/magento/magento2/issues/19609) -- config:set --lock-config does not act on other scopes (fixed in [magento/magento2#20322](https://github.com/magento/magento2/pull/20322)) + * [#19399](https://github.com/magento/magento2/issues/19399) -- Add product customization option collapsible design issue (fixed in [magento/magento2#19400](https://github.com/magento/magento2/pull/19400)) + * [#20120](https://github.com/magento/magento2/issues/20120) -- Review Details Detailed Rating misaligned (fixed in [magento/magento2#20272](https://github.com/magento/magento2/pull/20272)) + * [#20172](https://github.com/magento/magento2/issues/20172) -- On customer login page input field are short width on tablet view (fixed in [magento/magento2#20369](https://github.com/magento/magento2/pull/20369)) + * [#19085](https://github.com/magento/magento2/issues/19085) -- Translation in tier_price.phtml not working (fixed in [magento/magento2#19377](https://github.com/magento/magento2/pull/19377)) + * [#18361](https://github.com/magento/magento2/issues/18361) -- Customer last name is encoded twice in the XML interface (fixed in [magento/magento2#18362](https://github.com/magento/magento2/pull/18362)) + * [#19887](https://github.com/magento/magento2/issues/19887) -- creating new shipment: gettting all trackers. after this commit 2307e16 (fixed in [magento/magento2#20184](https://github.com/magento/magento2/pull/20184)) + * [#19985](https://github.com/magento/magento2/issues/19985) -- Send email confirmation popup close button area overlapping to content (fixed in [magento/magento2#20541](https://github.com/magento/magento2/pull/20541)) + * [#17759](https://github.com/magento/magento2/issues/17759) -- M2.2.5 : CustomerRepository::getList() does not load custom attribute if the name is "company" (fixed in [magento/magento2#20284](https://github.com/magento/magento2/pull/20284)) + * [#19800](https://github.com/magento/magento2/issues/19800) -- Contact us : design improvement (fixed in [magento/magento2#20455](https://github.com/magento/magento2/pull/20455)) + * [#19645](https://github.com/magento/magento2/issues/19645) -- Area Frontend: Account information page checkbox alignment issue. (fixed in [magento/magento2#20457](https://github.com/magento/magento2/pull/20457)) + * [#19791](https://github.com/magento/magento2/issues/19791) -- Logo vertical misalignment. (fixed in [magento/magento2#20456](https://github.com/magento/magento2/pull/20456)) + * [#15950](https://github.com/magento/magento2/issues/15950) -- Magento2 CSV product import qty and is_in_stock not working correct (fixed in [magento/magento2#20177](https://github.com/magento/magento2/pull/20177)) + * [#19899](https://github.com/magento/magento2/issues/19899) -- Credit memo for $0 order without refunded shipping produces negative credit memo (fixed in [magento/magento2#20508](https://github.com/magento/magento2/pull/20508)) + * [#20121](https://github.com/magento/magento2/issues/20121) -- Cancel order increases stock although "Set Items' Status to be In Stock When Order is Cancelled" is set to No (fixed in [magento/magento2#20547](https://github.com/magento/magento2/pull/20547)) + * [#18027](https://github.com/magento/magento2/issues/18027) -- Cart Total is NaN in some circumstances (fixed in [magento/magento2#20638](https://github.com/magento/magento2/pull/20638)) + * [#20376](https://github.com/magento/magento2/issues/20376) -- Image gets uploaded if field is disable in Category (fixed in [magento/magento2#20636](https://github.com/magento/magento2/pull/20636)) + * [#20169](https://github.com/magento/magento2/issues/20169) -- Admin user with restricted "order create" access can "view", "cancel", etc via API (fixed in [magento/magento2#20542](https://github.com/magento/magento2/pull/20542)) + * [#20399](https://github.com/magento/magento2/issues/20399) -- On wish list page edit, remove item misalign in 640 X 767 resolution (fixed in [magento/magento2#20544](https://github.com/magento/magento2/pull/20544)) + * [#20373](https://github.com/magento/magento2/issues/20373) -- Order view invoices template not display proper on ipad (fixed in [magento/magento2#20546](https://github.com/magento/magento2/pull/20546)) + * [#18387](https://github.com/magento/magento2/issues/18387) -- catalog:images:resize fails to process all images -> Possible underlying Magento/Framework/DB/Query/Generator issue (fixed in [magento/magento2#18809](https://github.com/magento/magento2/pull/18809)) + * [#18931](https://github.com/magento/magento2/issues/18931) -- Product added to shopping cart / comparison list message not translated by default (fixed in [magento/magento2#19461](https://github.com/magento/magento2/pull/19461)) + * [#14712](https://github.com/magento/magento2/issues/14712) -- Shipping issue on PayPal Express (fixed in [magento/magento2#19655](https://github.com/magento/magento2/pull/19655)) + * [#20113](https://github.com/magento/magento2/issues/20113) -- Widget option labels are misalinged (fixed in [magento/magento2#20270](https://github.com/magento/magento2/pull/20270)) + * [#20304](https://github.com/magento/magento2/issues/20304) -- No space between step title and saved address in checkout (fixed in [magento/magento2#20418](https://github.com/magento/magento2/pull/20418)) + * [#20609](https://github.com/magento/magento2/issues/20609) -- Currency rate value not align proper in order information tab when we create creditmemo from admin (fixed in [magento/magento2#20613](https://github.com/magento/magento2/pull/20613)) + * [#20500](https://github.com/magento/magento2/issues/20500) -- Recent Order Product Title Misaligned in Sidebar (fixed in [magento/magento2#20744](https://github.com/magento/magento2/pull/20744)) + * [#20563](https://github.com/magento/magento2/issues/20563) -- Go to shipping information, Update qty & Addresses and Enter a new address button Not aligned from left and right in 767px screen size (fixed in [magento/magento2#20739](https://github.com/magento/magento2/pull/20739)) + * [#19436](https://github.com/magento/magento2/issues/19436) -- Attribute Option with zero at the bigining does not work if there is already option with the same number without the zero (REST API)) (fixed in [magento/magento2#19612](https://github.com/magento/magento2/pull/19612)) + * [#20604](https://github.com/magento/magento2/issues/20604) -- Gift option message overlap edit and remove button (fixed in [magento/magento2#20784](https://github.com/magento/magento2/pull/20784)) + * [#20137](https://github.com/magento/magento2/issues/20137) -- On checkout page apply discount button is not align with input box (fixed in [magento/magento2#20837](https://github.com/magento/magento2/pull/20837)) + * [#20624](https://github.com/magento/magento2/issues/20624) -- `\Magento\ImportExport\Block\Adminhtml\Export\Filter::_getSelectHtmlWithValue()` method overwrites self $value argument (fixed in [magento/magento2#20863](https://github.com/magento/magento2/pull/20863)) + * [#20409](https://github.com/magento/magento2/issues/20409) -- Magento\Catalog\Api\ProductRenderListInterface returns products regardless of visibility (fixed in [magento/magento2#20886](https://github.com/magento/magento2/pull/20886)) + * [#20259](https://github.com/magento/magento2/issues/20259) -- Store switcher not sliding up and down, only dropdown arrow working (fixed in [magento/magento2#20540](https://github.com/magento/magento2/pull/20540)) +* GitHub pull requests: + * [magento/magento2#16361](https://github.com/magento/magento2/pull/16361) -- Allow usage of config-global.php when running Integration Tests (by @jissereitsma) + * [magento/magento2#16422](https://github.com/magento/magento2/pull/16422) -- Replace intval() function by using direct type casting to (int) where no default value is needed (by @mhauri) + * [magento/magento2#17708](https://github.com/magento/magento2/pull/17708) -- Prevent rendering of "Ship here" button if it is not needed (by @marvinhuebner) + * [magento/magento2#17783](https://github.com/magento/magento2/pull/17783) -- Current password autocomplete for admin login (by @flancer64) + * [magento/magento2#17885](https://github.com/magento/magento2/pull/17885) -- Make sure all linked products (related, upsells, crosssells) show up … (by @hostep) + * [magento/magento2#17891](https://github.com/magento/magento2/pull/17891) -- #17890: show correct text swatch values per store view (by @magicaner) + * [magento/magento2#17919](https://github.com/magento/magento2/pull/17919) -- [remove] rich snippet declaration on grouped product (by @AurelienLavorel) + * [magento/magento2#17945](https://github.com/magento/magento2/pull/17945) -- [2.2] return $this from setters in Analytics/ReportXml/DB/SelectBuilder.php (by @TBlindaruk) + * [magento/magento2#17966](https://github.com/magento/magento2/pull/17966) -- Fix currency symbol setting back to default #17567 (by @magently) + * [magento/magento2#17970](https://github.com/magento/magento2/pull/17970) -- Integration test for swatches types in attribute configuration added (by @rogyar) + * [magento/magento2#17990](https://github.com/magento/magento2/pull/17990) -- Menu does not work when you change from Mobile to Desktop mode #5402 (by @emanuelarcos) + * [magento/magento2#18001](https://github.com/magento/magento2/pull/18001) -- Fixes saving product in single-store mode if website_id <> 1 (by @eduard13) + * [magento/magento2#18002](https://github.com/magento/magento2/pull/18002) -- Fix module uninstall shell command and composer removal w/out regression (by @Thundar) + * [magento/magento2#18004](https://github.com/magento/magento2/pull/18004) -- [CatalogUrlRewrite] Covering the CategoryProcessUrlRewriteMovingObserver by Unit Test (by @eduard13) + * [magento/magento2#18018](https://github.com/magento/magento2/pull/18018) -- [Backport] Use route ID when creating secret keys in backend menus instead of route name #17650 (by @lfolco) + * [magento/magento2#18034](https://github.com/magento/magento2/pull/18034) -- [Backport] fix notice undefined shipment: revert locale inside loop (by @dmytro-ch) + * [magento/magento2#18127](https://github.com/magento/magento2/pull/18127) -- [Backport] typofix: ImportCollection -> ItemCollection (by @dmytro-ch) + * [magento/magento2#18137](https://github.com/magento/magento2/pull/18137) -- [2.2] Update labels section in README.md (by @sidolov) + * [magento/magento2#14065](https://github.com/magento/magento2/pull/14065) -- Correctly convert config integration api resources (by @therool) + * [magento/magento2#17679](https://github.com/magento/magento2/pull/17679) -- Update shipment collection to unserialize packages attribute after load (by @dnsv) + * [magento/magento2#17993](https://github.com/magento/magento2/pull/17993) -- fix #17582 ./bin/magento config:show fails with a fatal error (by @keyurshah070) + * [magento/magento2#18000](https://github.com/magento/magento2/pull/18000) -- Fix sitemap grid render incorrect base urls for multiple stores (by @nntoan) + * [magento/magento2#18055](https://github.com/magento/magento2/pull/18055) -- fix: reset search mini-form when we have no data / an empty response (by @DanielRuf) + * [magento/magento2#18097](https://github.com/magento/magento2/pull/18097) -- [Backport] Fix import grouped products #12853 (by @insanityinside) + * [magento/magento2#18113](https://github.com/magento/magento2/pull/18113) -- [Backport] Fixes from #15947 (by @ihor-sviziev) + * [magento/magento2#18098](https://github.com/magento/magento2/pull/18098) -- Fix shipping discount failed to apply during place order (by @torreytsui) + * [magento/magento2#18126](https://github.com/magento/magento2/pull/18126) -- [Backport] [2.2] Changed intval($val) to (int) $val, since it is faster: (by @dmytro-ch) + * [magento/magento2#17511](https://github.com/magento/magento2/pull/17511) -- Use cast types instead of xyzval() (by @sreichel) + * [magento/magento2#17998](https://github.com/magento/magento2/pull/17998) -- 9830 - Null order in Magento\Sales\Block\Order\PrintShipment.php (by @MateuszChrapek) + * [magento/magento2#17984](https://github.com/magento/magento2/pull/17984) -- Implemeted MAGETWO-81170: Missing $debugHintsPath when sending email … (by @passtet) + * [magento/magento2#18225](https://github.com/magento/magento2/pull/18225) -- Module Catalog: fix issue with custom option price conversion for different base currency on website level (by @oleksii-lisovyi) + * [magento/magento2#16885](https://github.com/magento/magento2/pull/16885) -- [Fix] Do not modify current list of countries with require states during setup upgrade (by @jalogut) + * [magento/magento2#18086](https://github.com/magento/magento2/pull/18086) -- Cast products "getStoreId()" to int, closes #18079 (by @sreichel) + * [magento/magento2#18215](https://github.com/magento/magento2/pull/18215) -- fix wysiwyg editor not decoding base64 filenames special chars (by @adammada) + * [magento/magento2#18280](https://github.com/magento/magento2/pull/18280) -- [Backport] Change sort order for customer group options (by @dmytro-ch) + * [magento/magento2#18168](https://github.com/magento/magento2/pull/18168) -- Fixed issue with lib-line-height mixin failing when value of 'normal'… (by @CNanninga) + * [magento/magento2#18310](https://github.com/magento/magento2/pull/18310) -- [Backport] Sales: add missing unit tests for model classes (by @dmytro-ch) + * [magento/magento2#18311](https://github.com/magento/magento2/pull/18311) -- [Backport] Added integration test for gift message quote merge (by @dmytro-ch) + * [magento/magento2#17695](https://github.com/magento/magento2/pull/17695) -- ConfigurableProduct show prices in select options (by @alexeya-ven) + * [magento/magento2#17982](https://github.com/magento/magento2/pull/17982) -- add error message in else condition (by @vaibhavahalpara) + * [magento/magento2#18354](https://github.com/magento/magento2/pull/18354) -- Fix for parsing attribute options labels, when & used. (by @bartoszkubicki) + * [magento/magento2#17882](https://github.com/magento/magento2/pull/17882) -- Do not overwrite URL Key with blank value (by @josephmcdermott) + * [magento/magento2#17986](https://github.com/magento/magento2/pull/17986) -- Implemented 17964: Backend Order creation Authorizenet: If invalid cr… (by @passtet) + * [magento/magento2#18283](https://github.com/magento/magento2/pull/18283) -- [Backport] Fix for removing the dirs while creating a TAR archive (by @haroldclaus) + * [magento/magento2#18369](https://github.com/magento/magento2/pull/18369) -- [Backport] Fix throwing error by checkout error processor model (by @ihor-sviziev) + * [magento/magento2#18375](https://github.com/magento/magento2/pull/18375) -- Backport 2.2 - Fix wrong reference in google analytics module layout xml (by @sambolek) + * [magento/magento2#18377](https://github.com/magento/magento2/pull/18377) -- [Backport 2.2-develop] Refactor Mass Order Cancel code to use Interface (by @JeroenVanLeusden) + * [magento/magento2#18376](https://github.com/magento/magento2/pull/18376) -- Backport 2.2 - Fix issue 17152 - prevent email being marked as not se… (by @sambolek) + * [magento/magento2#18391](https://github.com/magento/magento2/pull/18391) -- Backport 2.2 - Allow keyboard navigation in browser on product detail… (by @hostep) + * [magento/magento2#18400](https://github.com/magento/magento2/pull/18400) -- Admin Login Form > Aliging Label (by @rafaelstz) + * [magento/magento2#18414](https://github.com/magento/magento2/pull/18414) -- [Backport] Fix the issue with customer inline edit when password is expired (by @dmytro-ch) + * [magento/magento2#18415](https://github.com/magento/magento2/pull/18415) -- [Backport] Added unit test for CRON converter plugin (by @dmytro-ch) + * [magento/magento2#18426](https://github.com/magento/magento2/pull/18426) -- [Backport] Removed unnecessary characters from comments (by @lewisvoncken) + * [magento/magento2#18428](https://github.com/magento/magento2/pull/18428) -- [Backport] small misspelling fixed (by @lewisvoncken) + * [magento/magento2#18429](https://github.com/magento/magento2/pull/18429) -- [Backport] Fix documentation grammar errors and typos in actions.js (by @lewisvoncken) + * [magento/magento2#18430](https://github.com/magento/magento2/pull/18430) -- [Backport] Fix documentation typos in registry.js (by @lewisvoncken) + * [magento/magento2#18433](https://github.com/magento/magento2/pull/18433) -- [Backport] Improve code quality subscriber new action (by @lewisvoncken) + * [magento/magento2#18432](https://github.com/magento/magento2/pull/18432) -- [Backport] Removed commented code (by @lewisvoncken) + * [magento/magento2#17823](https://github.com/magento/magento2/pull/17823) -- [FEATURE] [issue-3283] Added Filter Support for Yes/No (boolean) attr… (by @lewisvoncken) + * [magento/magento2#18175](https://github.com/magento/magento2/pull/18175) -- Fix category tree in cart price rule #17493 (by @magently) + * [magento/magento2#18166](https://github.com/magento/magento2/pull/18166) -- Fix table rate failing for zip+4 address #17770 (by @magently) + * [magento/magento2#18389](https://github.com/magento/magento2/pull/18389) -- Backport 2.2 - Introducing a dedicated cron.log file for logging cron… (by @hostep) + * [magento/magento2#18390](https://github.com/magento/magento2/pull/18390) -- Backport 2.2 - Don't set a source model on the attribute when it's no… (by @hostep) + * [magento/magento2#18422](https://github.com/magento/magento2/pull/18422) -- [BACKPORT] Replace sort callbacks to spaceship operator (by @lewisvoncken) + * [magento/magento2#18403](https://github.com/magento/magento2/pull/18403) -- Fix setup wizard page logo (by @rafaelstz) + * [magento/magento2#18425](https://github.com/magento/magento2/pull/18425) -- [Backport] Fixing Snake Case To Camel Case (by @lewisvoncken) + * [magento/magento2#18427](https://github.com/magento/magento2/pull/18427) -- [Backport] Fix wrong return type in StockRegistryInterface API (by @lewisvoncken) + * [magento/magento2#15683](https://github.com/magento/magento2/pull/15683) -- Added checks to see if the payment is available (by @michielgerritsen) + * [magento/magento2#15905](https://github.com/magento/magento2/pull/15905) -- #4942 and bundle checkbox bug (by @JosephMaxwell) + * [magento/magento2#16115](https://github.com/magento/magento2/pull/16115) -- Fix type hint of customer-data updateSectionId parameters (by @Vinai) + * [magento/magento2#17516](https://github.com/magento/magento2/pull/17516) -- Feature australian regions (by @maximbaibakov) + * [magento/magento2#18155](https://github.com/magento/magento2/pull/18155) -- Fix type hint of @message declaration as the "setWidgetParameters" method allows arrays too (by @avstudnitz) + * [magento/magento2#18401](https://github.com/magento/magento2/pull/18401) -- Admin > Footer > Aligning Proportionally (by @rafaelstz) + * [magento/magento2#17968](https://github.com/magento/magento2/pull/17968) -- Fix Customer custom attributes lost after save (by @Thundar) + * [magento/magento2#18196](https://github.com/magento/magento2/pull/18196) -- Fix for custom product attribute changing 'backend_type' when 'is_user_defined = 1' and get updated/saved in Admin Backend (by @bartoszkubicki) + * [magento/magento2#18495](https://github.com/magento/magento2/pull/18495) -- [Backport] Checkout - Fix "Cannot read property 'code' on undefined" issue (by @ihor-sviziev) + * [magento/magento2#18552](https://github.com/magento/magento2/pull/18552) -- [Backport] Added validation on maximum quantity allowed in shopping cart (by @gelanivishal) + * [magento/magento2#18554](https://github.com/magento/magento2/pull/18554) -- [Backport] throw exception InvalidArgumentException during validate scheme (by @gelanivishal) + * [magento/magento2#18556](https://github.com/magento/magento2/pull/18556) -- [Backport] Fixed typo from filed to field (by @gelanivishal) + * [magento/magento2#18559](https://github.com/magento/magento2/pull/18559) -- [Backport] Covering the AssignOrderToCustomerObserver by Unit Test (by @gelanivishal) + * [magento/magento2#18564](https://github.com/magento/magento2/pull/18564) -- [Backport] Empty option Label should always be blank even if attribute is required (by @gelanivishal) + * [magento/magento2#18561](https://github.com/magento/magento2/pull/18561) -- [2.2] added component status based filtering (by @gelanivishal) + * [magento/magento2#18569](https://github.com/magento/magento2/pull/18569) -- [Backport] Make it possible to disable report bugs link (by @gelanivishal) + * [magento/magento2#18587](https://github.com/magento/magento2/pull/18587) -- [Backport] Prevent XSS on checkout (by @dmytro-ch) + * [magento/magento2#18586](https://github.com/magento/magento2/pull/18586) -- [Backport] Added missing throw tag for exception to docblock of construct (by @dmytro-ch) + * [magento/magento2#18593](https://github.com/magento/magento2/pull/18593) -- Calendar icon in advance pricing alignment solved (by @speedy008) + * [magento/magento2#18595](https://github.com/magento/magento2/pull/18595) -- [Backport] Fix disappearing navigation arrows in fotorama zoom (by @luukschakenraad) + * [magento/magento2#18599](https://github.com/magento/magento2/pull/18599) -- [Backport] Do not use new Phrase in Link Current class (by @dmytro-ch) + * [magento/magento2#18619](https://github.com/magento/magento2/pull/18619) -- [Backport] Add required fields to templates (by @miguelbalparda) + * [magento/magento2#18656](https://github.com/magento/magento2/pull/18656) -- [Backport] Fix product details causing Validation error (by @gelanivishal) + * [magento/magento2#18657](https://github.com/magento/magento2/pull/18657) -- [Backport] Create empty modelData array to avoid undefined var error (by @gelanivishal) + * [magento/magento2#18659](https://github.com/magento/magento2/pull/18659) -- [Backport] Fix for #12969 - server port detection for errors (by @gelanivishal) + * [magento/magento2#18662](https://github.com/magento/magento2/pull/18662) -- [Backport] move hardcoded MIME types from class private to DI configuration (by @gelanivishal) + * [magento/magento2#16915](https://github.com/magento/magento2/pull/16915) -- magento/magento2#14510: Creating custom customer attribute with default value 0 will cause not saving value for customer entity. (by @swnsma) + * [magento/magento2#18563](https://github.com/magento/magento2/pull/18563) -- [Backport] Update CategoryProcessor.php (by @gelanivishal) + * [magento/magento2#18566](https://github.com/magento/magento2/pull/18566) -- Module Catalog URL Rewrite: fix issue with product URL Rewrites re-generation after changing product URL Key for product with existing url_path attribute value (by @oleksii-lisovyi) + * [magento/magento2#18670](https://github.com/magento/magento2/pull/18670) -- Remove unnecessary class import, see #18280 (by @sreichel) + * [magento/magento2#18658](https://github.com/magento/magento2/pull/18658) -- [Backport] MAGENTO-18131: Fixed EAV attributes values query (by @gelanivishal) + * [magento/magento2#15366](https://github.com/magento/magento2/pull/15366) -- 15259 : Unable to disable without providing Industry value (by @sunilit42) + * [magento/magento2#18424](https://github.com/magento/magento2/pull/18424) -- [BACKPORT] type casted $qty to float in \Magento\Catalog\Model\Produc… (by @lewisvoncken) + * [magento/magento2#18660](https://github.com/magento/magento2/pull/18660) -- [Backport] Fix of saving "clone_field" fields (by @gelanivishal) + * [magento/magento2#18758](https://github.com/magento/magento2/pull/18758) -- [Backport] Fix the typo in PHPDoc comment (by @dmytro-ch) + * [magento/magento2#18535](https://github.com/magento/magento2/pull/18535) -- Fixed issues-18534: 2 wysiwyg on catalog category edit page (by @k1las) + * [magento/magento2#18597](https://github.com/magento/magento2/pull/18597) -- [Backport] Fix empty cart button (by @luukschakenraad) + * [magento/magento2#18604](https://github.com/magento/magento2/pull/18604) -- Fixed Issue: Special price of 0.0000 is not shown on frontend, but is calculated in cart (by @maheshWebkul721) + * [magento/magento2#18643](https://github.com/magento/magento2/pull/18643) -- Fix customer unsubscribed issue (by @janakbhimani) + * [magento/magento2#18759](https://github.com/magento/magento2/pull/18759) -- [Backport] Backend: add missing unit test for ModuleService class (by @dmytro-ch) + * [magento/magento2#16940](https://github.com/magento/magento2/pull/16940) -- Resolve incorrect scope code selection when the requested scopeCode is null (by @matthew-muscat) + * [magento/magento2#18737](https://github.com/magento/magento2/pull/18737) -- [BUGFIX] GITHUB-18264 Backport of #17799 for the 2.2 branch (by @kanduvisla) + * [magento/magento2#17971](https://github.com/magento/magento2/pull/17971) -- Don't format Special Price value for Bundle Product (by @magently) + * [magento/magento2#18681](https://github.com/magento/magento2/pull/18681) -- [Backport] Set fallback values for email and _website columns to avoid 'undefined index' error in CustomerComposite.php (by @TomashKhamlai) + * [magento/magento2#18833](https://github.com/magento/magento2/pull/18833) -- [Backport] Cover \Magento\GiftMessage\Observer\SalesEventQuoteMerge with Unit test (by @vasilii-b) + * [magento/magento2#18834](https://github.com/magento/magento2/pull/18834) -- [Backport] Cover \Magento\Email\Model\Template\SenderResolver class with Unit test (by @vasilii-b) + * [magento/magento2#18835](https://github.com/magento/magento2/pull/18835) -- [Backport] Added Unit Test for WindowsSmtpConfig Plugin (by @vasilii-b) + * [magento/magento2#18876](https://github.com/magento/magento2/pull/18876) -- [Backport] Fix Useless use of Cat (by @gelanivishal) + * [magento/magento2#18591](https://github.com/magento/magento2/pull/18591) -- [Backport] Fix SKU limit in import new products (by @ravi-chandra3197) + * [magento/magento2#18862](https://github.com/magento/magento2/pull/18862) -- [Backport] Adding trimming sku value function to sku backend model. (by @gelanivishal) + * [magento/magento2#18865](https://github.com/magento/magento2/pull/18865) -- fixed issue #18458 : Alert widget gets close when click anywhere on screen #18576 (by @Shubham-Webkul) + * [magento/magento2#18886](https://github.com/magento/magento2/pull/18886) -- [Backport] fixed Translation issue send-friend in send.phtml (by @rahulwebkul) + * [magento/magento2#18917](https://github.com/magento/magento2/pull/18917) -- Fixed-Global-search icon misaligned (by @speedy008) + * [magento/magento2#17978](https://github.com/magento/magento2/pull/17978) -- #17488 Fix Authenticating a customer via REST API does not update the last logged in data (by @prakashpatel07) + * [magento/magento2#18287](https://github.com/magento/magento2/pull/18287) -- Ensure integer values are not quoted as strings (by @udovicic) + * [magento/magento2#18874](https://github.com/magento/magento2/pull/18874) -- [Backport] Fixed issue #4468 "Unable to insert multiple catalog product list wid… (by @gelanivishal) + * [magento/magento2#18372](https://github.com/magento/magento2/pull/18372) -- Resolve typo despatch event (by @neeta-wagento) + * [magento/magento2#18863](https://github.com/magento/magento2/pull/18863) -- [Backport] #17744 Adding logic to get default billing address used on Cart and Checkout (by @gelanivishal) + * [magento/magento2#18872](https://github.com/magento/magento2/pull/18872) -- [Backport] Allow set billing information via API with existing address (by @gelanivishal) + * [magento/magento2#18870](https://github.com/magento/magento2/pull/18870) -- [Backport] ISSUE-5021 - fixed place order for custom shipping methods with under… (by @gelanivishal) + * [magento/magento2#18875](https://github.com/magento/magento2/pull/18875) -- [Backport] Sections LESS mixins: fix the issue with missing rules and incorrect default variables (by @gelanivishal) + * [magento/magento2#18873](https://github.com/magento/magento2/pull/18873) -- [Backport] Prevent exception when option text converts to false (by @gelanivishal) + * [magento/magento2#18967](https://github.com/magento/magento2/pull/18967) -- fixed - Magento 2.2.6 Default values are not rendering on Wishlist product edit page (by @webkul-ratnesh) + * [magento/magento2#18908](https://github.com/magento/magento2/pull/18908) -- [Backport] fixed - Unable to select payment method according to country of the address at checkout time (by @rahulwebkul) + * [magento/magento2#18984](https://github.com/magento/magento2/pull/18984) -- [Backport] Reload cart totals when cart data changes (by @tdgroot) + * [magento/magento2#16887](https://github.com/magento/magento2/pull/16887) -- Fix blocked a frame with origin (by @iGerchak) + * [magento/magento2#18857](https://github.com/magento/magento2/pull/18857) -- Fixed - Default tax region/state appears in customer & order data #16684 (by @ssp58bleuciel) + * [magento/magento2#18964](https://github.com/magento/magento2/pull/18964) -- Backport [PR 18772] Remove unnecesary "header" block redeclaration (by @samuel27m) + * [magento/magento2#19012](https://github.com/magento/magento2/pull/19012) -- #18348 - In admin, last swatch option set to default upon save (by @RostislavS) + * [magento/magento2#19036](https://github.com/magento/magento2/pull/19036) -- magento/magento2#18323: Order confirmation email for guest checkout d… (by @swnsma) + * [magento/magento2#18985](https://github.com/magento/magento2/pull/18985) -- [Backport] Added form fieldset before html data to \Magento\Framework\Data\Form\Element\Fieldset in getElementHtml() method (by @vasilii-b) + * [magento/magento2#19002](https://github.com/magento/magento2/pull/19002) -- [Backport] Remove duplicated CSS selector (by @dmytro-ch) + * [magento/magento2#19044](https://github.com/magento/magento2/pull/19044) -- [2.2-develop] magento/magento2#14007: "Use in Layered Navigation: Filterable (no results)" property confuse for Price filter (by @vpodorozh) + * [magento/magento2#19074](https://github.com/magento/magento2/pull/19074) -- [Backport] Fix for #12399: Exception Error in Catalog Price Rule while Backend language is not English (by @Mardl) + * [magento/magento2#18461](https://github.com/magento/magento2/pull/18461) -- fix Fatal Error when save configurable product in Magento 2.2.5 #18082 (by @thiagolima-bm) + * [magento/magento2#18649](https://github.com/magento/magento2/pull/18649) -- [Backport] Issue Fixed: Missing Fixed Product Tax total on PDF (by @maheshWebkul721) + * [magento/magento2#18815](https://github.com/magento/magento2/pull/18815) -- [Backoport] Issue Fixed: Backups error from User Roles Permission 2.2.6 (by @maheshWebkul721) + * [magento/magento2#19073](https://github.com/magento/magento2/pull/19073) -- magento/magento2#19071: Password strength indicator shows No Password… (by @dimasalamatov) + * [magento/magento2#19089](https://github.com/magento/magento2/pull/19089) -- magento/magento#18901: Forgot password form should not available while customer is logged in. (by @swnsma) + * [magento/magento2#19105](https://github.com/magento/magento2/pull/19105) -- magento/magento2#18840: Invalid Unit Test Annotations. (by @swnsma) + * [magento/magento2#19110](https://github.com/magento/magento2/pull/19110) -- [Backport] Add additional check if password hash is empty in auth process (by @agorbulin) + * [magento/magento2#14914](https://github.com/magento/magento2/pull/14914) -- FIX for issue #14849 - In Sales Emails no translation using order.getStatusLabel() (by @phoenix128) + * [magento/magento2#17854](https://github.com/magento/magento2/pull/17854) -- Fix translations of category design theme not being applied (by @cezary-zeglen) + * [magento/magento2#17915](https://github.com/magento/magento2/pull/17915) -- Fix/add expresion (by @magently) + * [magento/magento2#18743](https://github.com/magento/magento2/pull/18743) -- Fixed tierprice discount not calculated correctly if has specialprice (by @gelanivishal) + * [magento/magento2#18959](https://github.com/magento/magento2/pull/18959) -- fixed js translation (by @torhoehn) + * [magento/magento2#19118](https://github.com/magento/magento2/pull/19118) -- [Backport] Add/update newsletter messages in translation file (by @arnoudhgz) + * [magento/magento2#17889](https://github.com/magento/magento2/pull/17889) -- Fixed child items showing on My Account order view (by @rogyar) + * [magento/magento2#19113](https://github.com/magento/magento2/pull/19113) -- [2.2 backport] fix cipherMethod detection for openssl 1.1.1 (by @BlackIkeEagle) + * [magento/magento2#16342](https://github.com/magento/magento2/pull/16342) -- #14020-Cart-Sales-Rule-with-negated-condition-over-special-price-does… (by @novikor) + * [magento/magento2#18808](https://github.com/magento/magento2/pull/18808) -- fixed Quote Item Prices are NULL in cart related events. #18685 (by @ashutoshwebkul) + * [magento/magento2#19216](https://github.com/magento/magento2/pull/19216) -- [Backport] Covering the \Magento\Weee observers by Unit Tests (by @eduard13) + * [magento/magento2#19217](https://github.com/magento/magento2/pull/19217) -- [Backport] Covering the CheckUserLoginBackendObserver by Unit Test (by @eduard13) + * [magento/magento2#19237](https://github.com/magento/magento2/pull/19237) -- [Backport] #18956 Fixes for set root_category_id (by @gelanivishal) + * [magento/magento2#19240](https://github.com/magento/magento2/pull/19240) -- [Backport] Add missing unit test for WishlistSettings plugin (by @gelanivishal) + * [magento/magento2#19260](https://github.com/magento/magento2/pull/19260) -- Issue #19205 Fixed: Bundle Product Option with input type is checkbox and add to cart with 3 values only 2 values added to cart. (by @maheshWebkul721) + * [magento/magento2#18642](https://github.com/magento/magento2/pull/18642) -- [Backport] Fix issue with unexpected changing of subscription status after customer saving (by @alexeya-ven) + * [magento/magento2#18951](https://github.com/magento/magento2/pull/18951) -- Magento 2.2 Fix Product::addImageToMediaGallery throws Exception (by @progreg) + * [magento/magento2#18960](https://github.com/magento/magento2/pull/18960) -- local themes should be added to git repo (by @torhoehn) + * [magento/magento2#19068](https://github.com/magento/magento2/pull/19068) -- Using Media Image custom attribute type could not display on frontend. #19054 (by @Nazar65) + * [magento/magento2#19337](https://github.com/magento/magento2/pull/19337) -- [Backport] 19082-Fatal-error-Uncaught-Error-Cannot-call-abstract-method-Magento-… (by @agorbulin) + * [magento/magento2#19336](https://github.com/magento/magento2/pull/19336) -- [Backport] small performance improvement on product listing (by @gelanivishal) + * [magento/magento2#19338](https://github.com/magento/magento2/pull/19338) -- [Backport] missing use statement in layout generator (by @gelanivishal) + * [magento/magento2#19340](https://github.com/magento/magento2/pull/19340) -- [Backport] Fix the issue: Content overlaps the close button #19263 (by @gelanivishal) + * [magento/magento2#14485](https://github.com/magento/magento2/pull/14485) -- Fix for Issue #4136, MAGETWO-53440 (by @vasilii-b) + * [magento/magento2#18621](https://github.com/magento/magento2/pull/18621) -- 18615 updates structure for last_trans_id to be varchar 255 which is … (by @iancassidyweb) + * [magento/magento2#18905](https://github.com/magento/magento2/pull/18905) -- Fix the issue with missing asterisk for admin required fields (by @dmytro-ch) + * [magento/magento2#19296](https://github.com/magento/magento2/pull/19296) -- Fix issue 19286 - Wrong pager style (by @speedy008) + * [magento/magento2#19355](https://github.com/magento/magento2/pull/19355) -- [Backport] Changed get product way in blocks with related products (by @gelanivishal) + * [magento/magento2#19357](https://github.com/magento/magento2/pull/19357) -- [Backport] #13157 - Last Ordered Items block - bad js code (by @gelanivishal) + * [magento/magento2#19023](https://github.com/magento/magento2/pull/19023) -- [2.2 develop] [backport #19018] [issue #17833] child theme does not inherit translations from parent theme (by @vpodorozh) + * [magento/magento2#19358](https://github.com/magento/magento2/pull/19358) -- [Backport] Fix the issue with repetitive "tbody" tag for order items table (by @gelanivishal) + * [magento/magento2#19365](https://github.com/magento/magento2/pull/19365) -- Fixing a test for Magento Newsletter. (by @tiagosampaio) + * [magento/magento2#18899](https://github.com/magento/magento2/pull/18899) -- [Backport] fixed - can't import external http to https redirecting images by default csv import (by @rahulwebkul) + * [magento/magento2#19356](https://github.com/magento/magento2/pull/19356) -- [Backport] Magento backend Notifications counter round icon small cut from right side (by @gelanivishal) + * [magento/magento2#19364](https://github.com/magento/magento2/pull/19364) -- [Backport] fix: remove old code in tabs, always set tabindex to 0 when tabs are … (by @DanielRuf) + * [magento/magento2#19374](https://github.com/magento/magento2/pull/19374) -- back-port-pull-19024 (by @agorbulin) + * [magento/magento2#19014](https://github.com/magento/magento2/pull/19014) -- [Backport] #17813 - Huge "product_data_storage" in localStorage hangs the shop (by @omiroshnichenko) + * [magento/magento2#19398](https://github.com/magento/magento2/pull/19398) -- [Backport-2.2] Code generation improvement for php 7.1 (by @swnsma) + * [magento/magento2#19422](https://github.com/magento/magento2/pull/19422) -- Fix for incorrectly escapeHtml'd JSON in commit b8f78cc6 (by @insanityinside) + * [magento/magento2#19426](https://github.com/magento/magento2/pull/19426) -- [Backport] Fixing the customer subscribing from different stores (by @eduard13) + * [magento/magento2#19427](https://github.com/magento/magento2/pull/19427) -- [Backport] Adding integration tests for wrong captcha (by @eduard13) + * [magento/magento2#18922](https://github.com/magento/magento2/pull/18922) -- Fixed 18918 Asterisk sign display twice (by @suryakant-krish) + * [magento/magento2#19239](https://github.com/magento/magento2/pull/19239) -- [Backport] Allow to read HTTP/2 response header. (by @gelanivishal) + * [magento/magento2#19430](https://github.com/magento/magento2/pull/19430) -- Fixed issue with Base Currency for website is CND when PayPal Payflow Pro is charging in USD (by @Rykh) + * [magento/magento2#19431](https://github.com/magento/magento2/pull/19431) -- [Backport] Sample Link Issue in Downloadable product in magento-2.2.6 #19344 (by @ansari-krish) + * [magento/magento2#19447](https://github.com/magento/magento2/pull/19447) -- [Backport] chore: remove unused code in admin view of catalog (by @DanielRuf) + * [magento/magento2#19145](https://github.com/magento/magento2/pull/19145) -- Add availability to leave empty config for events.xml (by @lisovyievhenii) + * [magento/magento2#19568](https://github.com/magento/magento2/pull/19568) -- [Backport] [Newsletter] #19418 Cannot add additional field to system configuration at desired position (by @vasilii-b) + * [magento/magento2#19678](https://github.com/magento/magento2/pull/19678) -- [Backport] Fix: SalesQuoteSaveAfterObserver fails to update the checkout session quote id when applicable (by @dmytro-ch) + * [magento/magento2#19668](https://github.com/magento/magento2/pull/19668) -- [Backport] style: change b to strong (a11y) (by @DanielRuf) + * [magento/magento2#19669](https://github.com/magento/magento2/pull/19669) -- [Backport] fix: remove unused params in categorySubmit invocation (by @DanielRuf) + * [magento/magento2#19804](https://github.com/magento/magento2/pull/19804) -- [Backport]Fix issue 19796 - Sales Order invoice Update Qty's Button is misaligned (by @speedy008) + * [magento/magento2#19949](https://github.com/magento/magento2/pull/19949) -- [Backport] Fixed Issue #19917 Changed allowDrug to allowDrag (by @maheshWebkul721) + * [magento/magento2#19967](https://github.com/magento/magento2/pull/19967) -- [Backport] Minor typos corrected. (by @milindsingh) + * [magento/magento2#19970](https://github.com/magento/magento2/pull/19970) -- [Backport] Typo taax -> tax (by @milindsingh) + * [magento/magento2#19968](https://github.com/magento/magento2/pull/19968) -- [Backport] Typo "customet_id" to "customer_id" fixed. (by @milindsingh) + * [magento/magento2#19972](https://github.com/magento/magento2/pull/19972) -- [Backport] Update bootstrap.js (by @milindsingh) + * [magento/magento2#19971](https://github.com/magento/magento2/pull/19971) -- [Backport] Typo corrected Update bound-nodes.js (by @milindsingh) + * [magento/magento2#18912](https://github.com/magento/magento2/pull/18912) -- [Backport] Fixed subscribe to newsletter if you already have an account issue (by @ravi-chandra3197) + * [magento/magento2#19199](https://github.com/magento/magento2/pull/19199) -- [Backport][2.2] Made logo clickable on home page (by @gwharton) + * [magento/magento2#19280](https://github.com/magento/magento2/pull/19280) -- [BackPort] resolve typos and correct variable names (by @viral-wagento) + * [magento/magento2#19690](https://github.com/magento/magento2/pull/19690) -- [Backport] Additional Cache Management title (by @thomas-blackbird) + * [magento/magento2#19693](https://github.com/magento/magento2/pull/19693) -- [Backport] Cancel expired orders using OrderManagementInterface (by @JeroenVanLeusden) + * [magento/magento2#19911](https://github.com/magento/magento2/pull/19911) -- [Backport] fixed store wise product filter issue (by @shikhamis11) + * [magento/magento2#19945](https://github.com/magento/magento2/pull/19945) -- [Backport] issue 18941 (by @Nazar65) + * [magento/magento2#19056](https://github.com/magento/magento2/pull/19056) -- Fix issue 19052- Position order showing before the text box (by @speedy008) + * [magento/magento2#19910](https://github.com/magento/magento2/pull/19910) -- [Backport] fixed Notification page Select Visible items issue (by @shikhamis11) + * [magento/magento2#19889](https://github.com/magento/magento2/pull/19889) -- [Backport]Fix issue 19507 - Frontend Minicart dropdown alignment issue (by @speedy008) + * [magento/magento2#19928](https://github.com/magento/magento2/pull/19928) -- [Backport] [Review] Integration tests for not allowed review submission (by @eduard13) + * [magento/magento2#19989](https://github.com/magento/magento2/pull/19989) -- [Backport] Fixed #19605 Don't static compile disabled modules (by @shikhamis11) + * [magento/magento2#20081](https://github.com/magento/magento2/pull/20081) -- [Backport] Fixed issue - #19346 Import data 2.2.6 Value for 'product_type' attribute contains incorrect value (by @GovindaSharma) + * [magento/magento2#20080](https://github.com/magento/magento2/pull/20080) -- [Backport] Fixed Incorrect class name on Orders and returns page. (by @shikhamis11) + * [magento/magento2#20083](https://github.com/magento/magento2/pull/20083) -- [Backport] fixed issue #19925 Close button overlapping in shipping address label whenever any user adding new shipping address in mobile view in checkout (by @GovindaSharma) + * [magento/magento2#19423](https://github.com/magento/magento2/pull/19423) -- Fixed bug, when exception occurred on order with coupons cancel, made by guest after creating of customer account. (by @Winfle) + * [magento/magento2#19927](https://github.com/magento/magento2/pull/19927) -- [Backport] [Framework] New Link is not correctly shown as Current if contains default parts (by @eduard13) + * [magento/magento2#20082](https://github.com/magento/magento2/pull/20082) -- [Backport] issue resolved:Undefined Variable $itemsOrderItemId (by @milindsingh) + * [magento/magento2#20208](https://github.com/magento/magento2/pull/20208) -- magento/magento2:#19101 - API REST and Reserved Order Id (by @saphaljha) + * [magento/magento2#20219](https://github.com/magento/magento2/pull/20219) -- Changes-Hamburger-Icon-was-available-on-a-page (by @amol2jcommerce) + * [magento/magento2#20178](https://github.com/magento/magento2/pull/20178) -- magento/magento2#16198: Category image remain after deleted. (by @p-bystritsky) + * [magento/magento2#20183](https://github.com/magento/magento2/pull/20183) -- 2.2 develop pr port 18888 (by @saphaljha) + * [magento/magento2#20185](https://github.com/magento/magento2/pull/20185) -- [Backport] Move website_name column into columnSet (by @mage2pratik) + * [magento/magento2#20271](https://github.com/magento/magento2/pull/20271) -- [Backport] Use the new json serializer which throws an error when failing (by @quisse) + * [magento/magento2#20286](https://github.com/magento/magento2/pull/20286) -- [Backport] Don't return categoryId from registry if the product doesn't belong in the current category (by @GovindaSharma) + * [magento/magento2#20298](https://github.com/magento/magento2/pull/20298) -- ISSUE-20296: "@magentoDataIsolation" is used instead of "@magentoDbIsolation" in some integration tests. (by @p-bystritsky) + * [magento/magento2#20325](https://github.com/magento/magento2/pull/20325) -- [Backport] issus fixed #20158 Store switcher not aligned proper in tab view (by @shikhamis11) + * [magento/magento2#20328](https://github.com/magento/magento2/pull/20328) -- [Backport] Fix issue 20232 : Backend order credit card detail check box misaligned (by @GovindaSharma) + * [magento/magento2#20329](https://github.com/magento/magento2/pull/20329) -- [Backport] Product image failure when importing through CSV #20098 (by @irajneeshgupta) + * [magento/magento2#20353](https://github.com/magento/magento2/pull/20353) -- Fixed#20352: displaying html content for file type option on order view admin area (by @maheshWebkul721) + * [magento/magento2#19964](https://github.com/magento/magento2/pull/19964) -- [Backport] Fix the issue with reset password when customer has address from not allowed country (by @dmytro-ch) + * [magento/magento2#19984](https://github.com/magento/magento2/pull/19984) -- [Backport] Remove unneeded, also mistyped, saveHandler from CatalogSearch indexer declaration (by @dmytro-ch) + * [magento/magento2#20206](https://github.com/magento/magento2/pull/20206) -- 9130 remove the negative qty block. (by @saphaljha) + * [magento/magento2#20322](https://github.com/magento/magento2/pull/20322) -- issue #19609 Fixed for 2.2-develop (by @maheshWebkul721) + * [magento/magento2#19400](https://github.com/magento/magento2/pull/19400) -- [Backport]Fix-issue-19399-Add product customization option collapsible design issue (by @speedy008) + * [magento/magento2#20272](https://github.com/magento/magento2/pull/20272) -- Fixed-Review-Details-Detailed-Rating-misaligned (by @amol2jcommerce) + * [magento/magento2#20369](https://github.com/magento/magento2/pull/20369) -- 'Fixes-for-customer-login-page-input-field' :: On customer login page… (by @nainesh2jcommerce) + * [magento/magento2#20375](https://github.com/magento/magento2/pull/20375) -- [Backport] [Forwardport]Fix issue 19902 - Store View label and Dropdown misaligned (by @speedy008) + * [magento/magento2#20433](https://github.com/magento/magento2/pull/20433) -- [Backport] Missing echo of php vars in widget template file - tabshoriz.phtml (by @irajneeshgupta) + * [magento/magento2#20439](https://github.com/magento/magento2/pull/20439) -- [Backport] Meassage icon is not proper aligned (by @saphaljha) + * [magento/magento2#19377](https://github.com/magento/magento2/pull/19377) -- Back port pull #19094 (by @agorbulin) + * [magento/magento2#18362](https://github.com/magento/magento2/pull/18362) -- [Backport] fix(Webapi Xml Renderer - 18361): removed the not needed ampersand re… (by @nickshatilo) + * [magento/magento2#20184](https://github.com/magento/magento2/pull/20184) -- [Backport] Fix issue 19887 creating new shipment: getting all trackers. (by @mage2pratik) + * [magento/magento2#20505](https://github.com/magento/magento2/pull/20505) -- [Backport] Added constants to unit codes to make it easier to reuse it if necessary (by @mageprince) + * [magento/magento2#20509](https://github.com/magento/magento2/pull/20509) -- [Backport] Added required error message. (by @mageprince) + * [magento/magento2#20522](https://github.com/magento/magento2/pull/20522) -- [Backport] Add useful debug info for which website has not been found (by @mageprince) + * [magento/magento2#20541](https://github.com/magento/magento2/pull/20541) -- [Backport] Issue fixed #19985 Send email confirmation popup close button area ov… (by @irajneeshgupta) + * [magento/magento2#20284](https://github.com/magento/magento2/pull/20284) -- [Backport] Fix issue causing attribute not loading when using getList (by @GovindaSharma) + * [magento/magento2#20455](https://github.com/magento/magento2/pull/20455) -- [Backport] Fixed 19800 Contact us : design improvement (by @suryakant-krish) + * [magento/magento2#20456](https://github.com/magento/magento2/pull/20456) -- [Backport] Fixed 19791: Logo vertical misalignment. (by @suryakant-krish) + * [magento/magento2#20457](https://github.com/magento/magento2/pull/20457) -- [Backport] Area Frontend: Fixed checkbox alignment account information page. (by @suryakant-krish) + * [magento/magento2#20177](https://github.com/magento/magento2/pull/20177) -- magento/magento2#15950: Magento2 CSV product import qty and is_in_stock not working correct. (by @p-bystritsky) + * [magento/magento2#20508](https://github.com/magento/magento2/pull/20508) -- [Backport] Fix negative credit memo #19899 (by @mageprince) + * [magento/magento2#20547](https://github.com/magento/magento2/pull/20547) -- [Backport] Fixed Issue #20121 Cancel order increases stock although "Set Items' Status to be In Stock When Order is Cancelled" is set to No (by @irajneeshgupta) + * [magento/magento2#20636](https://github.com/magento/magento2/pull/20636) -- [Backport] Fix issue with file uploading if an upload field is disabled (by @serhiyzhovnir) + * [magento/magento2#20638](https://github.com/magento/magento2/pull/20638) -- [Backport] Floating point overflows in checkout totals fixed (by @shikhamis11) + * [magento/magento2#20647](https://github.com/magento/magento2/pull/20647) -- [Backport] fixed Negative order amount in dashboard (by @amol2jcommerce) + * [magento/magento2#20542](https://github.com/magento/magento2/pull/20542) -- [Backport] Order API resources updated. #20169 (by @irajneeshgupta) + * [magento/magento2#20544](https://github.com/magento/magento2/pull/20544) -- [Backport] 'wishlist-page-edit-remove-item-misalign' :: On wish list page edit, … (by @irajneeshgupta) + * [magento/magento2#20546](https://github.com/magento/magento2/pull/20546) -- [Backport] Order-view-invoices :: Order view invoices template not display prope… (by @irajneeshgupta) + * [magento/magento2#20685](https://github.com/magento/magento2/pull/20685) -- [Backport] update-button-issue-while-updating-billing-and-shipping-address (by @cmtickle) + * [magento/magento2#18809](https://github.com/magento/magento2/pull/18809) -- [Backport] catalog:images:resize total images count calculates incorrectly #18387 (by @vpodorozh) + * [magento/magento2#19461](https://github.com/magento/magento2/pull/19461) -- [Backport 2.2] issue #18931 fixed. (by @JeroenVanLeusden) + * [magento/magento2#19655](https://github.com/magento/magento2/pull/19655) -- Fixed - Shipping issue on PayPal Express #14712 (by @ssp58bleuciel) + * [magento/magento2#20285](https://github.com/magento/magento2/pull/20285) -- [Backport]#20222 Canary islands in ups carrier 2.2 (by @duckchip) + * [magento/magento2#20270](https://github.com/magento/magento2/pull/20270) -- [Backport] Fixed-Widget-option-labels-are-misalinged (by @amol2jcommerce) + * [magento/magento2#20418](https://github.com/magento/magento2/pull/20418) -- [Backport] issue fixed #20304 No space between step title and saved address in c… (by @shikhamis11) + * [magento/magento2#20613](https://github.com/magento/magento2/pull/20613) -- [Backport] admin-order-info-issue2.2 (by @dipti2jcommerce) + * [magento/magento2#20744](https://github.com/magento/magento2/pull/20744) -- [Backport] recent-order-product-title-misaligned (by @amol2jcommerce) + * [magento/magento2#20739](https://github.com/magento/magento2/pull/20739) -- [Backport] issue fixed #20563 Go to shipping information, Update qty & Addresses… (by @amol2jcommerce) + * [magento/magento2#19612](https://github.com/magento/magento2/pull/19612) -- [Backport] Fix: Attribute Option with zero at the beginning does not work if there is already option with the same number without the zero [REST API] (by @SikailoISM) + * [magento/magento2#19667](https://github.com/magento/magento2/pull/19667) -- [Backport] chore: remove old code for IE9 (by @DanielRuf) + * [magento/magento2#20642](https://github.com/magento/magento2/pull/20642) -- [Backport] magento/magento2#12194: Tier price on configurable product sorting so… (by @amol2jcommerce) + * [magento/magento2#20784](https://github.com/magento/magento2/pull/20784) -- [Backport] Gift-option-message-overlap-edit-and-remove-button-2.2 (by @ajay2jcommerce) + * [magento/magento2#20837](https://github.com/magento/magento2/pull/20837) -- [Backport] Fixed apply discount button alignment on checkout page (by @amol2jcommerce) + * [magento/magento2#20863](https://github.com/magento/magento2/pull/20863) -- [Backport] Update Filter.php fix issue #20624 (by @irajneeshgupta) + * [magento/magento2#20886](https://github.com/magento/magento2/pull/20886) -- [Backport] #20409 Fixed Unnecessary slash in namespace (by @milindsingh) + * [magento/magento2#20929](https://github.com/magento/magento2/pull/20929) -- resolve typo errors for js record.js (by @neeta-wagento) + * [magento/magento2#20540](https://github.com/magento/magento2/pull/20540) -- [Backport] issue fixed #20259 Store switcher not sliding up and down, only dropd… (by @irajneeshgupta) + 2.2.7 ============= * GitHub issues: diff --git a/app/bootstrap.php b/app/bootstrap.php index 8e901cac9bfb8..4a923cd0c910b 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -8,6 +8,7 @@ * Environment initialization */ error_reporting(E_ALL); +stream_wrapper_unregister('phar'); #ini_set('display_errors', 1); /* PHP version validation */ diff --git a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php index 6f0e42bdcbef1..e515fb3ccae6c 100644 --- a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php +++ b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php @@ -8,6 +8,9 @@ namespace Magento\AdminNotification\Block\Grid\Renderer; +/** + * Renderer class for action in the admin notifications grid. + */ class Actions extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** @@ -37,19 +40,23 @@ public function __construct( */ public function render(\Magento\Framework\DataObject $row) { - $readDetailsHtml = $row->getUrl() ? '' . + $readDetailsHtml = $row->getUrl() ? '' . __('Read Details') . '' : ''; - $markAsReadHtml = !$row->getIsRead() ? '' . __( - 'Mark as Read' - ) . '' : ''; + $markAsReadHtml = !$row->getIsRead() ? '' . __( + 'Mark as Read' + ) . '' : ''; $encodedUrl = $this->_urlHelper->getEncodedUrl(); return sprintf( - '%s%s%s', + '%s%s%s', $readDetailsHtml, $markAsReadHtml, $this->getUrl( diff --git a/app/code/Magento/AdminNotification/composer.json b/app/code/Magento/AdminNotification/composer.json index c577a2479f209..14afd21079f34 100644 --- a/app/code/Magento/AdminNotification/composer.json +++ b/app/code/Magento/AdminNotification/composer.json @@ -11,7 +11,7 @@ "lib-libxml": "*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" 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 index 407e323aeaae6..e30f1434dd23a 100644 --- 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 @@ -36,7 +36,7 @@ class AdditionalCommentTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment', 'getLabel']) + ->setMethods(['getComment', 'getLabel', 'getHtmlId', 'getName']) ->disableOriginalConstructor() ->getMock(); $this->contextMock = $this->getMockBuilder(Context::class) 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 index 54612076a757f..4adbb236ab952 100644 --- 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 @@ -37,7 +37,7 @@ class CollectionTimeLabelTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment']) + ->setMethods(['getComment', 'getHtmlId', 'getName']) ->disableOriginalConstructor() ->getMock(); $this->contextMock = $this->getMockBuilder(Context::class) 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 index 0806187ebac01..3e0307c9d86e1 100644 --- 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 @@ -51,7 +51,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment']) + ->setMethods(['getComment', 'getHtmlId', 'getName']) ->disableOriginalConstructor() ->getMock(); $this->formMock = $this->getMockBuilder(Form::class) 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 index 6a0cecc781062..8ca562385236a 100644 --- 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 @@ -36,7 +36,7 @@ class VerticalTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment', 'getLabel', 'getHint']) + ->setMethods(['getComment', 'getLabel', 'getHint', 'getHtmlId', 'getName']) ->disableOriginalConstructor() ->getMock(); $this->contextMock = $this->getMockBuilder(Context::class) diff --git a/app/code/Magento/Analytics/composer.json b/app/code/Magento/Analytics/composer.json index 3eebcafaba98f..11acfec76ca24 100644 --- a/app/code/Magento/Analytics/composer.json +++ b/app/code/Magento/Analytics/composer.json @@ -10,7 +10,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Authorizenet/Model/Directpost.php b/app/code/Magento/Authorizenet/Model/Directpost.php index 3057eaabde44f..6c5eeb1710170 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost.php +++ b/app/code/Magento/Authorizenet/Model/Directpost.php @@ -543,15 +543,16 @@ public function setResponseData(array $postData) public function validateResponse() { $response = $this->getResponse(); - //md5 check - if (!$this->getConfigData('trans_md5') - || !$this->getConfigData('login') - || !$response->isValidHash($this->getConfigData('trans_md5'), $this->getConfigData('login')) + $hashConfigKey = !empty($response->getData('x_SHA2_Hash')) ? 'signature_key' : 'trans_md5'; + + //hash check + if (!$response->isValidHash($this->getConfigData($hashConfigKey), $this->getConfigData('login')) ) { throw new \Magento\Framework\Exception\LocalizedException( __('The transaction was declined because the response hash validation failed.') ); } + return true; } diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request.php b/app/code/Magento/Authorizenet/Model/Directpost/Request.php index fc78d836b6080..dd35fd71c5a6d 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Request.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Request.php @@ -7,6 +7,7 @@ namespace Magento\Authorizenet\Model\Directpost; use Magento\Authorizenet\Model\Request as AuthorizenetRequest; +use Magento\Framework\Intl\DateTimeFactory; /** * Authorize.net request model for DirectPost model @@ -18,9 +19,33 @@ class Request extends AuthorizenetRequest */ protected $_transKey = null; + /** + * Hexadecimal signature key. + * + * @var string + */ + private $signatureKey = ''; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @param DateTimeFactory $dateTimeFactory + * @param array $data + */ + public function __construct( + DateTimeFactory $dateTimeFactory, + array $data = [] + ) { + $this->dateTimeFactory = $dateTimeFactory; + parent::__construct($data); + } + /** * Return merchant transaction key. - * Needed to generate sign. + * Needed to generate MD5 sign. * * @return string */ @@ -31,7 +56,7 @@ protected function _getTransactionKey() /** * Set merchant transaction key. - * Needed to generate sign. + * Needed to generate MD5 sign. * * @param string $transKey * @return $this @@ -43,7 +68,7 @@ protected function _setTransactionKey($transKey) } /** - * Generates the fingerprint for request. + * Generates the MD5 fingerprint for request. * * @param string $merchantApiLoginId * @param string $merchantTransactionKey @@ -63,7 +88,7 @@ public function generateRequestSign( ) { return hash_hmac( "md5", - $merchantApiLoginId . "^" . $fpSequence . "^" . $fpTimestamp . "^" . $amount . "^" . $currencyCode, + $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode, $merchantTransactionKey ); } @@ -85,6 +110,7 @@ public function setConstantData(\Magento\Authorizenet\Model\Directpost $paymentM ->setXRelayUrl($paymentMethod->getRelayUrl()); $this->_setTransactionKey($paymentMethod->getConfigData('trans_key')); + $this->setSignatureKey($paymentMethod->getConfigData('signature_key')); return $this; } @@ -168,17 +194,81 @@ public function setDataFromOrder( */ public function signRequestData() { - $fpTimestamp = time(); - $hash = $this->generateRequestSign( - $this->getXLogin(), - $this->_getTransactionKey(), - $this->getXAmount(), - $this->getXCurrencyCode(), - $this->getXFpSequence(), - $fpTimestamp - ); + $fpDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + $fpTimestamp = $fpDate->getTimestamp(); + + if (!empty($this->getSignatureKey())) { + $hash = $this->generateSha2RequestSign( + $this->getXLogin(), + $this->getSignatureKey(), + $this->getXAmount(), + $this->getXCurrencyCode(), + $this->getXFpSequence(), + $fpTimestamp + ); + } else { + $hash = $this->generateRequestSign( + $this->getXLogin(), + $this->_getTransactionKey(), + $this->getXAmount(), + $this->getXCurrencyCode(), + $this->getXFpSequence(), + $fpTimestamp + ); + } + $this->setXFpTimestamp($fpTimestamp); $this->setXFpHash($hash); + return $this; } + + /** + * Generates the SHA2 fingerprint for request. + * + * @param string $merchantApiLoginId + * @param string $merchantSignatureKey + * @param string $amount + * @param string $currencyCode + * @param string $fpSequence An invoice number or random number. + * @param string $fpTimestamp + * @return string The fingerprint. + */ + private function generateSha2RequestSign( + $merchantApiLoginId, + $merchantSignatureKey, + $amount, + $currencyCode, + $fpSequence, + $fpTimestamp + ): string { + $message = $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode; + + return strtoupper(hash_hmac('sha512', $message, pack('H*', $merchantSignatureKey))); + } + + /** + * Return merchant hexadecimal signature key. + * + * Needed to generate SHA2 sign. + * + * @return string + */ + private function getSignatureKey(): string + { + return $this->signatureKey; + } + + /** + * Set merchant hexadecimal signature key. + * + * Needed to generate SHA2 sign. + * + * @param string $signatureKey + * @return void + */ + private function setSignatureKey(string $signatureKey) + { + $this->signatureKey = $signatureKey; + } } diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Response.php b/app/code/Magento/Authorizenet/Model/Directpost/Response.php index dc62c1e990dc3..55e31f1526610 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Response.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Response.php @@ -24,25 +24,31 @@ class Response extends AuthorizenetResponse */ public function generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) { - if (!$amount) { - $amount = '0.00'; - } - return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); } /** * Return if is valid order id. * - * @param string $merchantMd5 + * @param string $storedHash * @param string $merchantApiLogin * @return bool */ - public function isValidHash($merchantMd5, $merchantApiLogin) + public function isValidHash($storedHash, $merchantApiLogin) { - $hash = $this->generateHash($merchantMd5, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); + if (empty($this->getData('x_amount'))) { + $this->setData('x_amount', '0.00'); + } - return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); + if (!empty($this->getData('x_SHA2_Hash'))) { + $hash = $this->generateSha2Hash($storedHash); + return Security::compareStrings($hash, $this->getData('x_SHA2_Hash')); + } elseif (!empty($this->getData('x_MD5_Hash'))) { + $hash = $this->generateHash($storedHash, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); + return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); + } + + return false; } /** @@ -54,4 +60,54 @@ public function isApproved() { return $this->getXResponseCode() == \Magento\Authorizenet\Model\Directpost::RESPONSE_CODE_APPROVED; } + + /** + * Generates an SHA2 hash to compare against AuthNet's. + * + * @param string $signatureKey + * @return string + * @see https://support.authorize.net/s/article/MD5-Hash-End-of-Life-Signature-Key-Replacement + */ + private function generateSha2Hash(string $signatureKey): string + { + $hashFields = [ + 'x_trans_id', + 'x_test_request', + 'x_response_code', + 'x_auth_code', + 'x_cvv2_resp_code', + 'x_cavv_response', + 'x_avs_code', + 'x_method', + 'x_account_number', + 'x_amount', + 'x_company', + 'x_first_name', + 'x_last_name', + 'x_address', + 'x_city', + 'x_state', + 'x_zip', + 'x_country', + 'x_phone', + 'x_fax', + 'x_email', + 'x_ship_to_company', + 'x_ship_to_first_name', + 'x_ship_to_last_name', + 'x_ship_to_address', + 'x_ship_to_city', + 'x_ship_to_state', + 'x_ship_to_zip', + 'x_ship_to_country', + 'x_invoice_num', + ]; + + $message = '^'; + foreach ($hashFields as $field) { + $message .= ($this->getData($field) ?? '') . '^'; + } + + return strtoupper(hash_hmac('sha512', $message, pack('H*', $signatureKey))); + } } diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php new file mode 100644 index 0000000000000..d3caa1597e64b --- /dev/null +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php @@ -0,0 +1,80 @@ +dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $dateTime = new \DateTime('2016-07-05 00:00:00', new \DateTimeZone('UTC')); + $this->dateTimeFactory->method('create') + ->willReturn($dateTime); + + $this->requestModel = new Request($this->dateTimeFactory); + } + + /** + * @param string $signatureKey + * @param string $expectedHash + * @dataProvider signRequestDataProvider + */ + public function testSignRequestData(string $signatureKey, string $expectedHash) + { + /** @var \Magento\Authorizenet\Model\Directpost $paymentMethod */ + $paymentMethod = $this->createMock(\Magento\Authorizenet\Model\Directpost::class); + $paymentMethod->method('getConfigData') + ->willReturnMap( + [ + ['test', null, true], + ['login', null, 'login'], + ['trans_key', null, 'trans_key'], + ['signature_key', null, $signatureKey], + ] + ); + + $this->requestModel->setConstantData($paymentMethod); + $this->requestModel->signRequestData(); + $signHash = $this->requestModel->getXFpHash(); + + $this->assertEquals($expectedHash, $signHash); + } + + /** + * @return array + */ + public function signRequestDataProvider() + { + return [ + [ + 'signatureKey' => '3EAFCE5697C1B4B9748385C1FCD29D86F3B9B41C7EED85A3A01DFF65' . + '70C8C29373C2A153355C3313CDF4AF723C0036DBF244A0821713A910024EE85547CEF37F', + 'expectedHash' => '719ED94DF5CF3510CB5531E8115462C8F12CBCC8E917BD809E8D40B4FF06' . + '1E14953554403DD9813CCCE0F31B184EB4DEF558E9C0747505A0C25420372DB00BE1' + ], + [ + 'signatureKey' => '', + 'expectedHash' => '3656211f2c41d1e4c083606f326c0460' + ], + ]; + } +} diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php index b4274e87401ca..0b616f32f9fe5 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php @@ -13,53 +13,16 @@ class ResponseTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Authorizenet\Model\Directpost\Response */ - protected $responseModel; + private $responseModel; protected function setUp() { $objectManager = new ObjectManager($this); - $this->responseModel = $objectManager->getObject(\Magento\Authorizenet\Model\Directpost\Response::class); - } - - /** - * @param string $merchantMd5 - * @param string $merchantApiLogin - * @param float|null $amount - * @param float|string $amountTestFunc - * @param string $transactionId - * @dataProvider generateHashDataProvider - */ - public function testGenerateHash($merchantMd5, $merchantApiLogin, $amount, $amountTestFunc, $transactionId) - { - $this->assertEquals( - $this->generateHash($merchantMd5, $merchantApiLogin, $amountTestFunc, $transactionId), - $this->responseModel->generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) + $this->responseModel = $objectManager->getObject( + \Magento\Authorizenet\Model\Directpost\Response::class ); } - /** - * @return array - */ - public function generateHashDataProvider() - { - return [ - [ - 'merchantMd5' => 'FCD7F001E9274FDEFB14BFF91C799306', - 'merchantApiLogin' => 'Magento', - 'amount' => null, - 'amountTestFunc' => '0.00', - 'transactionId' => '1' - ], - [ - 'merchantMd5' => '8AEF4E508261A287C3E2F544720FCA3A', - 'merchantApiLogin' => 'Magento2', - 'amount' => 100.50, - 'amountTestFunc' => 100.50, - 'transactionId' => '2' - ] - ]; - } - /** * @param $merchantMd5 * @param $merchantApiLogin @@ -74,7 +37,8 @@ protected function generateHash($merchantMd5, $merchantApiLogin, $amount, $trans } /** - * @param string $merchantMd5 + * @param string $storedHash + * @param string $hashKey * @param string $merchantApiLogin * @param float|null $amount * @param string $transactionId @@ -82,12 +46,21 @@ protected function generateHash($merchantMd5, $merchantApiLogin, $amount, $trans * @param bool $expectedValue * @dataProvider isValidHashDataProvider */ - public function testIsValidHash($merchantMd5, $merchantApiLogin, $amount, $transactionId, $hash, $expectedValue) - { + public function testIsValidHash( + string $storedHash, + string $hashKey, + string $merchantApiLogin, + $amount, + string $transactionId, + string $hash, + bool $expectedValue + ) { $this->responseModel->setXAmount($amount); $this->responseModel->setXTransId($transactionId); - $this->responseModel->setData('x_MD5_Hash', $hash); - $this->assertEquals($expectedValue, $this->responseModel->isValidHash($merchantMd5, $merchantApiLogin)); + $this->responseModel->setData($hashKey, $hash); + $result = $this->responseModel->isValidHash($storedHash, $merchantApiLogin); + + $this->assertEquals($expectedValue, $result); } /** @@ -95,9 +68,14 @@ public function testIsValidHash($merchantMd5, $merchantApiLogin, $amount, $trans */ public function isValidHashDataProvider() { + $signatureKey = '3EAFCE5697C1B4B9748385C1FCD29D86F3B9B41C7EED85A3A01DFF6570C8C' . + '29373C2A153355C3313CDF4AF723C0036DBF244A0821713A910024EE85547CEF37F'; + $expectedSha2Hash = '368D48E0CD1274BF41C059138DA69985594021A4AD5B4C5526AE88C8F' . + '7C5769B13C5E1E4358900F3E51076FB69D14B0A797904C22E8A11A52AA49CDE5FBB703C'; return [ [ 'merchantMd5' => 'FCD7F001E9274FDEFB14BFF91C799306', + 'hashKey' => 'x_MD5_Hash', 'merchantApiLogin' => 'Magento', 'amount' => null, 'transactionId' => '1', @@ -106,11 +84,21 @@ public function isValidHashDataProvider() ], [ 'merchantMd5' => '8AEF4E508261A287C3E2F544720FCA3A', + 'hashKey' => 'x_MD5_Hash', 'merchantApiLogin' => 'Magento2', 'amount' => 100.50, 'transactionId' => '2', 'hash' => '1F24A4EC9A169B2B2A072A5F168E16DC', 'expectedValue' => false + ], + [ + 'signatureKey' => $signatureKey, + 'hashKey' => 'x_SHA2_Hash', + 'merchantApiLogin' => 'Magento2', + 'amount' => 100.50, + 'transactionId' => '2', + 'hash' => $expectedSha2Hash, + 'expectedValue' => true ] ]; } diff --git a/app/code/Magento/Authorizenet/composer.json b/app/code/Magento/Authorizenet/composer.json index 0e6d1e8296c8a..d397f5cfae81e 100644 --- a/app/code/Magento/Authorizenet/composer.json +++ b/app/code/Magento/Authorizenet/composer.json @@ -16,7 +16,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "proprietary" ], diff --git a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml index 1319fa102d0d8..d6a935020a6dc 100644 --- a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml +++ b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml @@ -29,6 +29,10 @@ Magento\Config\Model\Config\Backend\Encrypted + + + Magento\Config\Model\Config\Backend\Encrypted + Magento\Config\Model\Config\Backend\Encrypted diff --git a/app/code/Magento/Authorizenet/etc/config.xml b/app/code/Magento/Authorizenet/etc/config.xml index 3a192646b6f7e..a8c747f208377 100644 --- a/app/code/Magento/Authorizenet/etc/config.xml +++ b/app/code/Magento/Authorizenet/etc/config.xml @@ -22,6 +22,7 @@ Credit Card Direct Post (Authorize.net) + 0 USD 1 diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php index 8e238ccab44cb..6c48eea66a951 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Graph.php +++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php @@ -15,7 +15,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * Api URL */ - const API_URL = 'http://chart.apis.google.com/chart'; + const API_URL = 'https://image-charts.com/chart'; /** * All series @@ -76,6 +76,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * Google chart api data encoding * + * @deprecated since the Google Image Charts API not accessible from March 14, 2019 * @var string */ protected $_encoding = 'e'; @@ -111,8 +112,8 @@ public function __construct( \Magento\Backend\Helper\Dashboard\Data $dashboardData, array $data = [] ) { - $this->_dashboardData = $dashboardData; parent::__construct($context, $collectionFactory, $data); + $this->_dashboardData = $dashboardData; } /** @@ -126,9 +127,9 @@ protected function _getTabTemplate() } /** - * Set data rows + * Set data rows. * - * @param array $rows + * @param string $rows * @return void */ public function setDataRows($rows) @@ -149,18 +150,18 @@ public function addSeries($seriesId, array $options) } /** - * Get series + * Get series. * * @param string $seriesId - * @return array|false + * @return array|bool */ public function getSeries($seriesId) { if (isset($this->_allSeries[$seriesId])) { return $this->_allSeries[$seriesId]; - } else { - return false; } + + return false; } /** @@ -187,11 +188,12 @@ public function getChartUrl($directUrl = true) { $params = [ 'cht' => 'lc', - 'chf' => 'bg,s,ffffff', - 'chco' => 'ef672f', 'chls' => '7', - 'chxs' => '0,676056,15,0,l,676056|1,676056,15,0,l,676056', - 'chm' => 'h,f2ebde,0,0:1:.1,1,-1', + 'chf' => 'bg,s,f4f4f4|c,lg,90,ffffff,0.1,ededed,0', + 'chm' => 'B,f4d4b2,0,0,0', + 'chco' => 'db4814', + 'chxs' => '0,0,11|1,0,11', + 'chma' => '15,15,15,15', ]; $this->_allSeries = $this->getRowsData($this->_dataRows); @@ -279,20 +281,11 @@ public function getChartUrl($directUrl = true) $this->_axisLabels['x'] = $dates; $this->_allSeries = $datas; - //Google encoding values - if ($this->_encoding == "s") { - // simple encoding - $params['chd'] = "s:"; - $dataDelimiter = ""; - $dataSetdelimiter = ","; - $dataMissing = "_"; - } else { - // extended encoding - $params['chd'] = "e:"; - $dataDelimiter = ""; - $dataSetdelimiter = ","; - $dataMissing = "__"; - } + // Image-Charts Awesome data format values + $params['chd'] = "a:"; + $dataDelimiter = ","; + $dataSetdelimiter = "|"; + $dataMissing = "_"; // process each string in the array, and find the max length $localmaxvalue = [0]; @@ -306,7 +299,6 @@ public function getChartUrl($directUrl = true) $minvalue = min($localminvalue); // default values - $yrange = 0; $yLabels = []; $miny = 0; $maxy = 0; @@ -314,14 +306,13 @@ public function getChartUrl($directUrl = true) if ($minvalue >= 0 && $maxvalue >= 0) { if ($maxvalue > 10) { - $p = pow(10, $this->_getPow($maxvalue)); + $p = pow(10, $this->_getPow((int)$maxvalue)); $maxy = ceil($maxvalue / $p) * $p; $yLabels = range($miny, $maxy, $p); } else { $maxy = ceil($maxvalue + 1); $yLabels = range($miny, $maxy, 1); } - $yrange = $maxy; $yorigin = 0; } @@ -329,44 +320,14 @@ public function getChartUrl($directUrl = true) foreach ($this->getAllSeries() as $index => $serie) { $thisdataarray = $serie; - if ($this->_encoding == "s") { - // SIMPLE ENCODING - for ($j = 0; $j < sizeof($thisdataarray); $j++) { - $currentvalue = $thisdataarray[$j]; - if (is_numeric($currentvalue)) { - $ylocation = round( - (strlen($this->_simpleEncoding) - 1) * ($yorigin + $currentvalue) / $yrange - ); - $chartdata[] = substr($this->_simpleEncoding, $ylocation, 1) . $dataDelimiter; - } else { - $chartdata[] = $dataMissing . $dataDelimiter; - } - } - } else { - // EXTENDED ENCODING - for ($j = 0; $j < sizeof($thisdataarray); $j++) { - $currentvalue = $thisdataarray[$j]; - if (is_numeric($currentvalue)) { - if ($yrange) { - $ylocation = 4095 * ($yorigin + $currentvalue) / $yrange; - } else { - $ylocation = 0; - } - $firstchar = floor($ylocation / 64); - $secondchar = $ylocation % 64; - $mappedchar = substr( - $this->_extendedEncoding, - $firstchar, - 1 - ) . substr( - $this->_extendedEncoding, - $secondchar, - 1 - ); - $chartdata[] = $mappedchar . $dataDelimiter; - } else { - $chartdata[] = $dataMissing . $dataDelimiter; - } + $count = count($thisdataarray); + for ($j = 0; $j < $count; $j++) { + $currentvalue = $thisdataarray[$j]; + if (is_numeric($currentvalue)) { + $ylocation = $yorigin + $currentvalue; + $chartdata[] = $ylocation . $dataDelimiter; + } else { + $chartdata[] = $dataMissing . $dataDelimiter; } } $chartdata[] = $dataSetdelimiter; @@ -381,45 +342,13 @@ public function getChartUrl($directUrl = true) $valueBuffer = []; - if (sizeof($this->_axisLabels) > 0) { + if (count($this->_axisLabels) > 0) { $params['chxt'] = implode(',', array_keys($this->_axisLabels)); $indexid = 0; foreach ($this->_axisLabels as $idx => $labels) { if ($idx == 'x') { - /** - * Format date - */ - foreach ($this->_axisLabels[$idx] as $_index => $_label) { - if ($_label != '') { - $period = new \DateTime($_label, new \DateTimeZone($timezoneLocal)); - switch ($this->getDataHelper()->getParam('period')) { - case '24h': - $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( - $period->setTime($period->format('H'), 0, 0), - \IntlDateFormatter::NONE, - \IntlDateFormatter::SHORT - ); - break; - case '7d': - case '1m': - $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( - $period, - \IntlDateFormatter::SHORT, - \IntlDateFormatter::NONE - ); - break; - case '1y': - case '2y': - $this->_axisLabels[$idx][$_index] = date('m/Y', strtotime($_label)); - break; - } - } else { - $this->_axisLabels[$idx][$_index] = ''; - } - } - + $this->formatAxisLabelDate($idx, $timezoneLocal); $tmpstring = implode('|', $this->_axisLabels[$idx]); - $valueBuffer[] = $indexid . ":|" . $tmpstring; } elseif ($idx == 'y') { $valueBuffer[] = $indexid . ":|" . implode('|', $yLabels); @@ -438,12 +367,52 @@ public function getChartUrl($directUrl = true) foreach ($params as $name => $value) { $p[] = $name . '=' . urlencode($value); } + return self::API_URL . '?' . implode('&', $p); - } else { - $gaData = urlencode(base64_encode(json_encode($params))); - $gaHash = $this->_dashboardData->getChartDataHash($gaData); - $params = ['ga' => $gaData, 'h' => $gaHash]; - return $this->getUrl('adminhtml/*/tunnel', ['_query' => $params]); + } + $gaData = urlencode(base64_encode(json_encode($params))); + $gaHash = $this->_dashboardData->getChartDataHash($gaData); + $params = ['ga' => $gaData, 'h' => $gaHash]; + + return $this->getUrl('adminhtml/*/tunnel', ['_query' => $params]); + } + + /** + * Format dates for axis labels. + * + * @param string $idx + * @param string $timezoneLocal + * @return void + */ + private function formatAxisLabelDate(string $idx, string $timezoneLocal) + { + foreach ($this->_axisLabels[$idx] as $_index => $_label) { + if ($_label != '') { + $period = new \DateTime($_label, new \DateTimeZone($timezoneLocal)); + switch ($this->getDataHelper()->getParam('period')) { + case '24h': + $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( + $period->setTime((int)$period->format('H'), 0, 0), + \IntlDateFormatter::NONE, + \IntlDateFormatter::SHORT + ); + break; + case '7d': + case '1m': + $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( + $period, + \IntlDateFormatter::SHORT, + \IntlDateFormatter::NONE + ); + break; + case '1y': + case '2y': + $this->_axisLabels[$idx][$_index] = date('m/Y', strtotime($_label)); + break; + } + } else { + $this->_axisLabels[$idx][$_index] = ''; + } } } @@ -540,6 +509,8 @@ protected function getHeight() } /** + * Sets data helper. + * * @param \Magento\Backend\Helper\Dashboard\AbstractDashboard $dataHelper * @return void */ diff --git a/app/code/Magento/Backend/Block/System/Design/Edit.php b/app/code/Magento/Backend/Block/System/Design/Edit.php index 4d6c26e4cfe4b..d2d3035f62e3d 100644 --- a/app/code/Magento/Backend/Block/System/Design/Edit.php +++ b/app/code/Magento/Backend/Block/System/Design/Edit.php @@ -66,7 +66,7 @@ protected function _prepareLayout() 'label' => __('Delete'), 'onclick' => 'deleteConfirm(\'' . __( 'Are you sure?' - ) . '\', \'' . $this->getDeleteUrl() . '\')', + ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'class' => 'delete' ] ); 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 3d7154eb20f92..37f11754529e5 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 @@ -9,11 +9,12 @@ * Store render group * * @author Magento Core Team + * @deprecated since Store Grid is refactored with UI Components */ class Group extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** - * {@inheritdoc} + * @inheritdoc */ public function render(\Magento\Framework\DataObject $row) { 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 9cfc8bfc52691..9be630277d4e5 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 @@ -9,11 +9,12 @@ * Store render store * * @author Magento Core Team + * @deprecated since Store Grid is refactored with UI Components */ class Store extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** - * {@inheritdoc} + * @inheritdoc */ public function render(\Magento\Framework\DataObject $row) { 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 487eb4f8acfda..d4bb213dce14e 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 @@ -9,11 +9,12 @@ * Store render website * * @author Magento Core Team + * @deprecated since Store Grid is refactored with UI Components */ class Website extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** - * {@inheritdoc} + * @inheritdoc */ public function render(\Magento\Framework\DataObject $row) { diff --git a/app/code/Magento/Backend/Block/System/Store/Store.php b/app/code/Magento/Backend/Block/System/Store/Store.php index cdbf55426de39..2145dc524259e 100644 --- a/app/code/Magento/Backend/Block/System/Store/Store.php +++ b/app/code/Magento/Backend/Block/System/Store/Store.php @@ -12,6 +12,7 @@ * @author Magento Core Team * @api * @since 100.0.2 + * @deprecated since Store Grid is refactored with UI Components */ class Store extends \Magento\Backend\Block\Widget\Grid\Container { diff --git a/app/code/Magento/Backend/Block/Widget/Form/Container.php b/app/code/Magento/Backend/Block/Widget/Form/Container.php index 8b7babc1bb9b6..d7bd785367347 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Container.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Container.php @@ -57,6 +57,7 @@ class Container extends \Magento\Backend\Block\Widget\Container /** * @return void + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _construct() { @@ -83,7 +84,7 @@ protected function _construct() -1 ); - $objId = $this->getRequest()->getParam($this->_objectId); + $objId = (int)$this->getRequest()->getParam($this->_objectId); if (!empty($objId)) { $this->addButton( @@ -93,7 +94,7 @@ protected function _construct() 'class' => 'delete', 'onclick' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->getDeleteUrl() . '\')' + ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})' ] ); } @@ -155,7 +156,7 @@ public function getBackUrl() */ public function getDeleteUrl() { - return $this->getUrl('*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId)]); + return $this->getUrl('*/*/delete', [$this->_objectId => (int)$this->getRequest()->getParam($this->_objectId)]); } /** 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 ddabeb90921c2..968e34d211cfd 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php @@ -281,24 +281,23 @@ public function getGridIdsJson() if (!$this->getUseSelectAll()) { return ''; } - /** @var \Magento\Framework\Data\Collection $allIdsCollection */ - $allIdsCollection = clone $this->getParentBlock()->getCollection(); - if ($this->getMassactionIdField()) { - $massActionIdField = $this->getMassactionIdField(); + /** @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection $collection */ + $collection = clone $this->getParentBlock()->getCollection(); + + if ($collection instanceof AbstractDb) { + $idsSelect = clone $collection->getSelect(); + $idsSelect->reset(\Magento\Framework\DB\Select::ORDER); + $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT); + $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET); + $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS); + $idsSelect->columns($this->getMassactionIdField(), 'main_table'); + $idList = $collection->getConnection()->fetchCol($idsSelect); } else { - $massActionIdField = $this->getParentBlock()->getMassactionIdField(); + $idList = $collection->setPageSize(0)->getColumnValues($this->getMassactionIdField()); } - if ($allIdsCollection instanceof AbstractDb) { - $allIdsCollection->getSelect()->limit(); - $allIdsCollection->clear(); - } - - $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); - if (!empty($gridIds)) { - return join(",", $gridIds); - } - return ''; + + return implode(',', $idList); } /** diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php index 21f28188cf874..6ccd7efff4d36 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php @@ -10,9 +10,14 @@ class Delete extends \Magento\Backend\Controller\Adminhtml\System\Design { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $id = $this->getRequest()->getParam('id'); if ($id) { $design = $this->_objectManager->create(\Magento\Framework\App\DesignInterface::class)->load($id); diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminAccountActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminAccountActionGroup.xml new file mode 100644 index 0000000000000..01e3450b78c85 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminAccountActionGroup.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/Data/CountryOptionsConfigData.xml b/app/code/Magento/Backend/Test/Mftf/Data/CountryOptionsConfigData.xml new file mode 100644 index 0000000000000..593d3831b643f --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Data/CountryOptionsConfigData.xml @@ -0,0 +1,20 @@ + + + + + DefaultAllowCountries + + + 0 + + + + US + + diff --git a/app/code/Magento/Backend/Test/Mftf/Metadata/country_options_config-meta.xml b/app/code/Magento/Backend/Test/Mftf/Metadata/country_options_config-meta.xml new file mode 100644 index 0000000000000..c54e99ff65d06 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Metadata/country_options_config-meta.xml @@ -0,0 +1,24 @@ + + + + + + + + + string + + integer + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml new file mode 100644 index 0000000000000..2f04c2c11d288 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminPopupModalSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminPopupModalSection.xml index 4ea184598663f..4cd6dbe5d8584 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminPopupModalSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminPopupModalSection.xml @@ -10,5 +10,6 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml new file mode 100644 index 0000000000000..b9570ce945943 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
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 e8143b5f6b43a..e62b73f39241d 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 @@ -269,62 +269,6 @@ public function testGetGridIdsJsonWithoutUseSelectAll() $this->assertEmpty($this->_block->getGridIdsJson()); } - /** - * @param array $items - * @param string $result - * - * @dataProvider dataProviderGetGridIdsJsonWithUseSelectAll - */ - public function testGetGridIdsJsonWithUseSelectAll(array $items, $result) - { - $this->_block->setUseSelectAll(true); - - if ($this->_block->getMassactionIdField()) { - $massActionIdField = $this->_block->getMassactionIdField(); - } else { - $massActionIdField = $this->_block->getParentBlock()->getMassactionIdField(); - } - - $collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->_gridMock->expects($this->once()) - ->method('getCollection') - ->willReturn($collectionMock); - $collectionMock->expects($this->once()) - ->method('setPageSize') - ->with(0) - ->willReturnSelf(); - $collectionMock->expects($this->once()) - ->method('getColumnValues') - ->with($massActionIdField) - ->willReturn($items); - - $this->assertEquals($result, $this->_block->getGridIdsJson()); - } - - /** - * @return array - */ - public function dataProviderGetGridIdsJsonWithUseSelectAll() - { - return [ - [ - [], - '', - ], - [ - [1], - '1', - ], - [ - [1, 2, 3], - '1,2,3', - ], - ]; - } - /** * @param string $itemId * @param array|\Magento\Framework\DataObject $item diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index dfd71f4ecd4d0..d3b766da0006f 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -24,7 +24,7 @@ "magento/module-theme": "100.2.*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index e3411166ee4a4..44ab1dcc00176 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -153,15 +153,15 @@ - + Magento\Config\Model\Config\Source\Yesno - + Magento\Config\Model\Config\Source\Yesno - + Magento\Config\Model\Config\Source\Yesno Minification is not applied in developer mode. @@ -169,11 +169,11 @@ - + Magento\Config\Model\Config\Source\Yesno - + Magento\Config\Model\Config\Source\Yesno Minification is not applied in developer mode. diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_index.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_index.xml index 7fdfe4044a3da..de2f7080a0db8 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_index.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_index.xml @@ -6,11 +6,10 @@ */ --> - - + - + diff --git a/app/code/Magento/Backup/composer.json b/app/code/Magento/Backup/composer.json index b613170b6d661..6e741ae29cab5 100644 --- a/app/code/Magento/Backup/composer.json +++ b/app/code/Magento/Backup/composer.json @@ -9,7 +9,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Backup/etc/adminhtml/system.xml b/app/code/Magento/Backup/etc/adminhtml/system.xml index 90f6fa861b40f..aa6635b4dde4a 100644 --- a/app/code/Magento/Backup/etc/adminhtml/system.xml +++ b/app/code/Magento/Backup/etc/adminhtml/system.xml @@ -26,6 +26,7 @@ 1 + 1 Magento\Backup\Model\Config\Source\Type @@ -33,12 +34,14 @@ 1 + 1 1 + 1 Magento\Cron\Model\Config\Source\Frequency Magento\Backup\Model\Config\Backend\Cron @@ -48,6 +51,7 @@ Please put your store into maintenance mode during backup. 1 + 1 Magento\Config\Model\Config\Source\Yesno diff --git a/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php b/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php index 1440d495e8a7d..09be3723e4518 100644 --- a/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php +++ b/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php @@ -74,7 +74,7 @@ public function execute() $this->logger->critical($e); $this->messageManager->addExceptionMessage( $e, - 'The order #' . $quote->getReservedOrderId() . ' cannot be processed.' + __('The order #%1 cannot be processed.', $quote->getReservedOrderId()) ); } diff --git a/app/code/Magento/Braintree/Controller/Paypal/Review.php b/app/code/Magento/Braintree/Controller/Paypal/Review.php index 4576e3b033df8..15acb1859ec87 100644 --- a/app/code/Magento/Braintree/Controller/Paypal/Review.php +++ b/app/code/Magento/Braintree/Controller/Paypal/Review.php @@ -11,6 +11,7 @@ use Magento\Braintree\Gateway\Config\PayPal\Config; use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; use Magento\Framework\Exception\LocalizedException; +use Magento\Payment\Model\Method\Logger; /** * Class Review @@ -22,6 +23,11 @@ class Review extends AbstractAction */ private $quoteUpdater; + /** + * @var Logger + */ + private $logger; + /** * @var string */ @@ -34,15 +40,18 @@ class Review extends AbstractAction * @param Config $config * @param Session $checkoutSession * @param QuoteUpdater $quoteUpdater + * @param Logger $logger */ public function __construct( Context $context, Config $config, Session $checkoutSession, - QuoteUpdater $quoteUpdater + QuoteUpdater $quoteUpdater, + Logger $logger ) { parent::__construct($context, $config, $checkoutSession); $this->quoteUpdater = $quoteUpdater; + $this->logger = $logger; } /** @@ -54,6 +63,7 @@ public function execute() $this->getRequest()->getPostValue('result', '{}'), true ); + $this->logger->debug($requestData); $quote = $this->checkoutSession->getQuote(); try { diff --git a/app/code/Magento/Braintree/Model/Multishipping/PlaceOrder.php b/app/code/Magento/Braintree/Model/Multishipping/PlaceOrder.php new file mode 100644 index 0000000000000..8c8ea2ea69691 --- /dev/null +++ b/app/code/Magento/Braintree/Model/Multishipping/PlaceOrder.php @@ -0,0 +1,177 @@ +orderManagement = $orderManagement; + $this->paymentExtensionFactory = $paymentExtensionFactory; + $this->getPaymentNonceCommand = $getPaymentNonceCommand; + } + + /** + * @inheritdoc + */ + public function place(array $orderList): array + { + if (empty($orderList)) { + return []; + } + + $errorList = []; + $firstOrder = $this->orderManagement->place(array_shift($orderList)); + // get payment token from first placed order + $paymentToken = $this->getPaymentToken($firstOrder); + + foreach ($orderList as $order) { + try { + /** @var OrderInterface $order */ + $orderPayment = $order->getPayment(); + $this->setVaultPayment($orderPayment, $paymentToken); + $this->orderManagement->place($order); + } catch (\Exception $e) { + $incrementId = $order->getIncrementId(); + $errorList[$incrementId] = $e; + } + } + + return $errorList; + } + + /** + * Sets vault payment method. + * + * @param OrderPaymentInterface $orderPayment + * @param PaymentTokenInterface $paymentToken + * @return void + */ + private function setVaultPayment(OrderPaymentInterface $orderPayment, PaymentTokenInterface $paymentToken) + { + $vaultMethod = $this->getVaultPaymentMethod( + $orderPayment->getMethod() + ); + $orderPayment->setMethod($vaultMethod); + + $publicHash = $paymentToken->getPublicHash(); + $customerId = $paymentToken->getCustomerId(); + $result = $this->getPaymentNonceCommand->execute( + ['public_hash' => $publicHash, 'customer_id' => $customerId] + ) + ->get(); + + $orderPayment->setAdditionalInformation( + DataAssignObserver::PAYMENT_METHOD_NONCE, + $result['paymentMethodNonce'] + ); + $orderPayment->setAdditionalInformation( + PaymentTokenInterface::PUBLIC_HASH, + $publicHash + ); + $orderPayment->setAdditionalInformation( + PaymentTokenInterface::CUSTOMER_ID, + $customerId + ); + } + + /** + * Returns vault payment method. + * + * For placing sequence of orders, we need to replace the original method on the vault method. + * + * @param string $method + * @return string + */ + private function getVaultPaymentMethod(string $method): string + { + $vaultPaymentMap = [ + ConfigProvider::CODE => ConfigProvider::CC_VAULT_CODE, + PaypalConfigProvider::PAYPAL_CODE => PaypalConfigProvider::PAYPAL_VAULT_CODE + ]; + + return $vaultPaymentMap[$method] ?? $method; + } + + /** + * Returns payment token. + * + * @param OrderInterface $order + * @return PaymentTokenInterface + * @throws \BadMethodCallException + */ + private function getPaymentToken(OrderInterface $order): PaymentTokenInterface + { + $orderPayment = $order->getPayment(); + $extensionAttributes = $this->getExtensionAttributes($orderPayment); + $paymentToken = $extensionAttributes->getVaultPaymentToken(); + + if ($paymentToken === null) { + throw new \BadMethodCallException('Vault Payment Token should be defined for placed order payment.'); + } + + return $paymentToken; + } + + /** + * Gets payment extension attributes. + * + * @param OrderPaymentInterface $payment + * @return OrderPaymentExtensionInterface + */ + private function getExtensionAttributes(OrderPaymentInterface $payment): OrderPaymentExtensionInterface + { + $extensionAttributes = $payment->getExtensionAttributes(); + if (null === $extensionAttributes) { + $extensionAttributes = $this->paymentExtensionFactory->create(); + $payment->setExtensionAttributes($extensionAttributes); + } + + return $extensionAttributes; + } +} diff --git a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php index aa23fa767d1ed..ae2b1b1423640 100644 --- a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php +++ b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php @@ -123,8 +123,8 @@ private function updateShippingAddress(Quote $quote, array $details) { $shippingAddress = $quote->getShippingAddress(); - $shippingAddress->setLastname($details['lastName']); - $shippingAddress->setFirstname($details['firstName']); + $shippingAddress->setLastname($this->getShippingRecipientLastName($details)); + $shippingAddress->setFirstname($this->getShippingRecipientFirstName($details)); $shippingAddress->setEmail($details['email']); $shippingAddress->setCollectShippingRates(true); @@ -188,4 +188,30 @@ private function updateAddressData(Address $address, array $addressData) $address->setSameAsBilling(false); $address->setCustomerAddressId(null); } + + /** + * Returns shipping recipient first name. + * + * @param array $details + * @return string + */ + private function getShippingRecipientFirstName(array $details) + { + return isset($details['shippingAddress']['recipientName']) + ? explode(' ', $details['shippingAddress']['recipientName'], 2)[0] + : $details['firstName']; + } + + /** + * Returns shipping recipient last name. + * + * @param array $details + * @return string + */ + private function getShippingRecipientLastName(array $details) + { + return isset($details['shippingAddress']['recipientName']) + ? explode(' ', $details['shippingAddress']['recipientName'], 2)[1] + : $details['lastName']; + } } diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontPayWithPaypalFromMiniCartActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontPayWithPaypalFromMiniCartActionGroup.xml new file mode 100644 index 0000000000000..8ad866b77e7ff --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontPayWithPaypalFromMiniCartActionGroup.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml index 4e8526a2b0e2b..aa0f5a936fd7e 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml @@ -71,6 +71,13 @@ + + DefaultActiveBraintreePaypal + + + 0 + + EnabledTitle AuthorizePaymentAction @@ -106,6 +113,13 @@ Magneto + + EnableActiveBraintreePaypal + + + 1 + + MasterCard 5105105105105100 diff --git a/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml b/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml index 0f734e5c02d56..04b3cf38d27a7 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml @@ -49,6 +49,9 @@ integer + + integer + diff --git a/app/code/Magento/Braintree/Test/Mftf/Page/StorefrontPaypalReviewOrderPage.xml b/app/code/Magento/Braintree/Test/Mftf/Page/StorefrontPaypalReviewOrderPage.xml new file mode 100644 index 0000000000000..82c0fa2a075d4 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Page/StorefrontPaypalReviewOrderPage.xml @@ -0,0 +1,15 @@ + + + + + +
+
+ + diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontMiniCartSection.xml new file mode 100644 index 0000000000000..bd4a5b72daa8a --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontPaypalReviewOrderItemsSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontPaypalReviewOrderItemsSection.xml new file mode 100644 index 0000000000000..8c834f7003eb5 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontPaypalReviewOrderItemsSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontPaypalReviewOrderShippingSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontPaypalReviewOrderShippingSection.xml new file mode 100644 index 0000000000000..2e4e6047f57bb --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontPaypalReviewOrderShippingSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php index cb911a8396b36..cc79b5b008e6e 100644 --- a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Braintree\Test\Unit\Controller\Paypal; +use Magento\Payment\Model\Method\Logger; use Magento\Quote\Model\Quote; use Magento\Framework\View\Layout; use Magento\Checkout\Model\Session; @@ -63,6 +64,14 @@ class ReviewTest extends \PHPUnit\Framework\TestCase * @var Review */ private $review; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + + /** + * @var Logger|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; protected function setUp() { @@ -87,6 +96,9 @@ protected function setUp() ->getMock(); $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) ->getMockForAbstractClass(); + $this->loggerMock = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); $contextMock->expects(self::once()) ->method('getRequest') @@ -102,7 +114,8 @@ protected function setUp() $contextMock, $this->configMock, $this->checkoutSessionMock, - $this->quoteUpdaterMock + $this->quoteUpdaterMock, + $this->loggerMock ); } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php index 80d333db80f0a..6925e37b580ac 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php @@ -111,7 +111,7 @@ public function testBuild() * @expectedException \Magento\Payment\Gateway\Command\CommandException * @expectedExceptionMessage The Payment Token is not available to perform the request. */ - public function testBuildWithoutPaymentToken(): void + public function testBuildWithoutPaymentToken() { $amount = 30.00; $buildSubject = [ diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php index 7475d81a56142..b67f7d09941ca 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php @@ -165,7 +165,7 @@ private function getDetails(): array 'region' => 'IL', 'postalCode' => '60618', 'countryCodeAlpha2' => 'US', - 'recipientName' => 'John Doe', + 'recipientName' => 'Jane Smith', ], 'billingAddress' => [ 'streetAddress' => '123 Billing Street', @@ -186,9 +186,9 @@ private function getDetails(): array private function updateShippingAddressStep(array $details) { $this->shippingAddress->method('setLastname') - ->with($details['lastName']); + ->with('Smith'); $this->shippingAddress->method('setFirstname') - ->with($details['firstName']); + ->with('Jane'); $this->shippingAddress->method('setEmail') ->with($details['email']); $this->shippingAddress->method('setCollectShippingRates') diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json index 8830333ac4f7f..0b9d76bc2ee9e 100644 --- a/app/code/Magento/Braintree/composer.json +++ b/app/code/Magento/Braintree/composer.json @@ -18,6 +18,7 @@ "magento/module-quote": "101.0.*", "magento/module-paypal": "100.2.*", "magento/module-ui": "101.0.*", + "magento/module-multishipping": "100.2.*", "braintree/braintree_php": "3.28.0" }, "suggest": { @@ -25,7 +26,7 @@ "magento/module-theme": "100.2.*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "proprietary" ], diff --git a/app/code/Magento/Braintree/etc/config.xml b/app/code/Magento/Braintree/etc/config.xml index a830c29368755..fe4cfab9c0e30 100644 --- a/app/code/Magento/Braintree/etc/config.xml +++ b/app/code/Magento/Braintree/etc/config.xml @@ -42,6 +42,7 @@ cc_type,cc_number,avsPostalCodeResponseCode,avsStreetAddressResponseCode,cvvResponseCode,processorAuthorizationCode,processorResponseCode,processorResponseText,liabilityShifted,liabilityShiftPossible,riskDataId,riskDataDecision Magento\Braintree\Model\AvsEmsCodeMapper Magento\Braintree\Model\CvvEmsCodeMapper + braintree_group BraintreePayPalFacade @@ -67,6 +68,7 @@ processorResponseCode,processorResponseText,paymentId processorResponseCode,processorResponseText,paymentId,payerEmail en_US,en_GB,en_AU,da_DK,fr_FR,fr_CA,de_DE,zh_HK,it_IT,nl_NL,no_NO,pl_PL,es_ES,sv_SE,tr_TR,pt_BR,ja_JP,id_ID,ko_KR,pt_PT,ru_RU,th_TH,zh_CN,zh_TW + braintree_group BraintreeCreditCardVaultFacade @@ -76,6 +78,7 @@ Magento\Braintree\Model\InstantPurchase\CreditCard\TokenFormatter Magento\Braintree\Model\InstantPurchase\PaymentAdditionalInformationProvider + braintree_group BraintreePayPalVaultFacade @@ -85,6 +88,7 @@ Magento\Braintree\Model\InstantPurchase\PayPal\TokenFormatter Magento\Braintree\Model\InstantPurchase\PaymentAdditionalInformationProvider + braintree_group diff --git a/app/code/Magento/Braintree/etc/frontend/di.xml b/app/code/Magento/Braintree/etc/frontend/di.xml index ea417c407dffd..d8d3a93b71dc3 100644 --- a/app/code/Magento/Braintree/etc/frontend/di.xml +++ b/app/code/Magento/Braintree/etc/frontend/di.xml @@ -61,4 +61,12 @@ Magento\Braintree\Model\LocaleResolver + + + + Magento\Braintree\Model\Multishipping\PlaceOrder + Magento\Braintree\Model\Multishipping\PlaceOrder + + + diff --git a/app/code/Magento/Braintree/etc/payment.xml b/app/code/Magento/Braintree/etc/payment.xml new file mode 100644 index 0000000000000..4cae049aaf5a9 --- /dev/null +++ b/app/code/Magento/Braintree/etc/payment.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + 1 + + + 1 + + + diff --git a/app/code/Magento/Braintree/view/frontend/layout/multishipping_checkout_billing.xml b/app/code/Magento/Braintree/view/frontend/layout/multishipping_checkout_billing.xml new file mode 100644 index 0000000000000..06390d403e63d --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/layout/multishipping_checkout_billing.xml @@ -0,0 +1,20 @@ + + + + + + + + Magento_Braintree::multishipping/form.phtml + Magento_Braintree::multishipping/form_paypal.phtml + + + + + diff --git a/app/code/Magento/Braintree/view/frontend/templates/multishipping/form.phtml b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form.phtml new file mode 100644 index 0000000000000..bf8aa8dd09c2c --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form.phtml @@ -0,0 +1,29 @@ + + + diff --git a/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml new file mode 100644 index 0000000000000..ea3eb2214c2d8 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml @@ -0,0 +1,29 @@ + + + diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/hosted-fields.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/hosted-fields.js new file mode 100644 index 0000000000000..1ceebc8e66282 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/hosted-fields.js @@ -0,0 +1,102 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*browser:true*/ +/*global define*/ + +define([ + 'jquery', + 'Magento_Braintree/js/view/payment/method-renderer/hosted-fields', + 'Magento_Braintree/js/validator', + 'Magento_Ui/js/model/messageList', + 'mage/translate', + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/model/payment/additional-validators' +], function ( + $, + Component, + validator, + messageList, + $t, + fullScreenLoader, + setPaymentInformationAction, + additionalValidators +) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_Braintree/payment/multishipping/form' + }, + + /** + * Get list of available CC types + * + * @returns {Object} + */ + getCcAvailableTypes: function () { + var availableTypes = validator.getAvailableCardTypes(), + billingCountryId; + + billingCountryId = $('#multishipping_billing_country_id').val(); + + if (billingCountryId && validator.getCountrySpecificCardTypes(billingCountryId)) { + return validator.collectTypes( + availableTypes, validator.getCountrySpecificCardTypes(billingCountryId) + ); + } + + return availableTypes; + }, + + /** + * @override + */ + placeOrder: function () { + var self = this; + + this.validatorManager.validate(self, function () { + return self.setPaymentInformation(); + }); + }, + + /** + * @override + */ + setPaymentInformation: function () { + if (additionalValidators.validate()) { + + fullScreenLoader.startLoader(); + + $.when( + setPaymentInformationAction( + this.messageContainer, + this.getData() + ) + ).done(this.done.bind(this)) + .fail(this.fail.bind(this)); + } + }, + + /** + * {Function} + */ + fail: function () { + fullScreenLoader.stopLoader(); + + return this; + }, + + /** + * {Function} + */ + done: function () { + fullScreenLoader.stopLoader(); + $('#multishipping-billing-form').submit(); + + return this; + } + }); +}); diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js new file mode 100644 index 0000000000000..6702e58d1214b --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js @@ -0,0 +1,143 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*browser:true*/ +/*global define*/ +define([ + 'jquery', + 'underscore', + 'Magento_Braintree/js/view/payment/method-renderer/paypal', + 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/model/payment/additional-validators', + 'Magento_Checkout/js/model/full-screen-loader', + 'mage/translate' +], function ( + $, + _, + Component, + setPaymentInformationAction, + additionalValidators, + fullScreenLoader, + $t +) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_Braintree/payment/multishipping/paypal', + submitButtonSelector: '#payment-continue span' + }, + + /** + * @override + */ + onActiveChange: function (isActive) { + this.updateSubmitButtonTitle(isActive); + + this._super(isActive); + }, + + /** + * @override + */ + beforePlaceOrder: function (data) { + this._super(data); + + this.updateSubmitButtonTitle(true); + }, + + /** + * @override + */ + getShippingAddress: function () { + return {}; + }, + + /** + * @override + */ + getData: function () { + var data = this._super(); + + data['additional_data']['is_active_payment_token_enabler'] = true; + + return data; + }, + + /** + * @override + */ + isActiveVault: function () { + return true; + }, + + /** + * Skipping order review step on checkout with multiple addresses is not allowed. + * + * @returns {Boolean} + */ + isSkipOrderReview: function () { + return false; + }, + + /** + * Checks if payment method nonce is already received. + * + * @returns {Boolean} + */ + isPaymentMethodNonceReceived: function () { + return this.paymentMethodNonce !== null; + }, + + /** + * Updates submit button title on multi-addresses checkout billing form. + * + * @param {Boolean} isActive + */ + updateSubmitButtonTitle: function (isActive) { + var title = this.isPaymentMethodNonceReceived() || !isActive ? + $t('Go to Review Your Order') : $t('Continue to PayPal'); + + $(this.submitButtonSelector).html(title); + }, + + /** + * @override + */ + placeOrder: function () { + if (!this.isPaymentMethodNonceReceived()) { + this.payWithPayPal(); + } else { + fullScreenLoader.startLoader(); + + $.when( + setPaymentInformationAction( + this.messageContainer, + this.getData() + ) + ).done(this.done.bind(this)) + .fail(this.fail.bind(this)); + } + }, + + /** + * {Function} + */ + fail: function () { + fullScreenLoader.stopLoader(); + + return this; + }, + + /** + * {Function} + */ + done: function () { + fullScreenLoader.stopLoader(); + $('#multishipping-billing-form').submit(); + + return this; + } + }); +}); diff --git a/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/form.html b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/form.html new file mode 100644 index 0000000000000..964e15df166d3 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/form.html @@ -0,0 +1,106 @@ + + +
+
+
+
+ + + +
+
+
+
    + +
  • + + + +
  • + +
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ + + +
+
+
+
+ +
+ +
+ +
+
+ +
+
+
+
\ No newline at end of file diff --git a/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/paypal.html b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/paypal.html new file mode 100644 index 0000000000000..722989e41f98f --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/paypal.html @@ -0,0 +1,40 @@ + +
+
+ +
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+
+
diff --git a/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php new file mode 100644 index 0000000000000..058b3a981b52f --- /dev/null +++ b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php @@ -0,0 +1,63 @@ +layout = $layout; + } + + /** + * Format tier price string + * + * @param Product $selection + * @param array $arguments + * @return string + */ + public function renderTierPrice(Product $selection, array $arguments = []): string + { + if (!array_key_exists('zone', $arguments)) { + $arguments['zone'] = Render::ZONE_ITEM_OPTION; + } + + $priceHtml = ''; + + /** @var Render $priceRender */ + $priceRender = $this->layout->getBlock('product.price.render.default'); + if ($priceRender !== false) { + $priceHtml = $priceRender->render( + TierPrice::PRICE_CODE, + $selection, + $arguments + ); + } + + return $priceHtml; + } +} diff --git a/app/code/Magento/Bundle/Model/Product/Price.php b/app/code/Magento/Bundle/Model/Product/Price.php index 00b6b2d7a3f5a..c9ed97fada966 100644 --- a/app/code/Magento/Bundle/Model/Product/Price.php +++ b/app/code/Magento/Bundle/Model/Product/Price.php @@ -11,8 +11,11 @@ use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; /** + * Bundle product type price model + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Price extends \Magento\Catalog\Model\Product\Type\Price @@ -180,9 +183,9 @@ protected function getBundleSelectionIds(\Magento\Catalog\Model\Product $product /** * Get product final price * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @param float $qty + * @param \Magento\Catalog\Model\Product $product + * @return float */ public function getFinalPrice($qty, $product) { @@ -207,9 +210,9 @@ public function getFinalPrice($qty, $product) * Returns final price of a child product * * @param \Magento\Catalog\Model\Product $product - * @param float $productQty + * @param float $productQty * @param \Magento\Catalog\Model\Product $childProduct - * @param float $childProductQty + * @param float $childProductQty * @return float */ public function getChildFinalPrice($product, $productQty, $childProduct, $childProductQty) @@ -220,10 +223,10 @@ public function getChildFinalPrice($product, $productQty, $childProduct, $childP /** * Retrieve Price considering tier price * - * @param \Magento\Catalog\Model\Product $product - * @param string|null $which - * @param bool|null $includeTax - * @param bool $takeTierPrice + * @param \Magento\Catalog\Model\Product $product + * @param string|null $which + * @param bool|null $includeTax + * @param bool $takeTierPrice * @return float|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -402,8 +405,8 @@ public function getOptions($product) * * @param \Magento\Catalog\Model\Product $bundleProduct * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float|null $selectionQty - * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity + * @param float|null $selectionQty + * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity * @return float * * @see \Magento\Bundle\Model\Product\Price::getSelectionFinalTotalPrice() @@ -418,7 +421,7 @@ public function getSelectionPrice($bundleProduct, $selectionProduct, $selectionQ * * @param \Magento\Catalog\Model\Product $bundleProduct * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float $qty + * @param float $qty * @return float */ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qty = null) @@ -427,15 +430,14 @@ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qt } /** - * Calculate final price of selection - * with take into account tier price + * Calculate final price of selection with take into account tier price * - * @param \Magento\Catalog\Model\Product $bundleProduct - * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float $bundleQty - * @param float $selectionQty - * @param bool $multiplyQty - * @param bool $takeTierPrice + * @param \Magento\Catalog\Model\Product $bundleProduct + * @param \Magento\Catalog\Model\Product $selectionProduct + * @param float $bundleQty + * @param float $selectionQty + * @param bool $multiplyQty + * @param bool $takeTierPrice * @return float */ public function getSelectionFinalTotalPrice( @@ -454,7 +456,11 @@ public function getSelectionFinalTotalPrice( } if ($bundleProduct->getPriceType() == self::PRICE_TYPE_DYNAMIC) { - $price = $selectionProduct->getFinalPrice($takeTierPrice ? $selectionQty : 1); + $totalQty = $bundleQty * $selectionQty; + if (!$takeTierPrice || $totalQty === 0) { + $totalQty = 1; + } + $price = $selectionProduct->getFinalPrice($totalQty); } else { if ($selectionProduct->getSelectionPriceType()) { // percent @@ -485,10 +491,10 @@ public function getSelectionFinalTotalPrice( /** * Apply tier price for bundle * - * @param \Magento\Catalog\Model\Product $product - * @param float $qty - * @param float $finalPrice - * @return float + * @param \Magento\Catalog\Model\Product $product + * @param float $qty + * @param float $finalPrice + * @return float */ protected function _applyTierPrice($product, $qty, $finalPrice) { @@ -509,9 +515,9 @@ protected function _applyTierPrice($product, $qty, $finalPrice) /** * Get product tier price by qty * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float|array + * @param float $qty + * @param \Magento\Catalog\Model\Product $product + * @return float|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -605,11 +611,11 @@ public function getTierPrice($qty, $product) /** * Calculate and apply special price * - * @param float $finalPrice - * @param float $specialPrice + * @param float $finalPrice + * @param float $specialPrice * @param string $specialPriceFrom * @param string $specialPriceTo - * @param mixed $store + * @param mixed $store * @return float */ public function calculateSpecialPrice( @@ -634,7 +640,7 @@ public function calculateSpecialPrice( * * @param /Magento/Catalog/Model/Product $bundleProduct * @param float|string $price - * @param int $bundleQty + * @param int $bundleQty * @return float */ public function getLowestPrice($bundleProduct, $price, $bundleQty = 1) diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml index 72140cf6d5848..2444776065f7e 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml @@ -106,4 +106,67 @@ + + + + + + + + + 10 + + + + 20 + + + + + {{productName}} + + + + + Drop-down Option + + + + Radio Buttons Option + + + + Checkbox Option + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml index 473c3036cab2c..a5c70c24e3d9b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml @@ -15,5 +15,9 @@ + + + +
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml new file mode 100644 index 0000000000000..89423c2c63658 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml @@ -0,0 +1,107 @@ + + + + + + + + + + <testCaseId value="MAGETWO-99047"/> + <useCaseId value="MAGETWO-96898"/> + <group value="catalog"/> + <group value="bundle"/> + </annotations> + <before> + <!-- Create Dynamic Bundle product --> + <actionGroup ref="AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup" stepKey="createBundleProduct"/> + + <!-- Add tier prices to simple products --> + <!-- Simple product 1 --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct1CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct1"/> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct1"> + <argument name="website" value="All Websites [USD]"/> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="5"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="50"/> + </actionGroup> + <!-- Simple product 2 --> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct2CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct2"/> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct2"> + <argument name="website" value="All Websites [USD]"/> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="7"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="25"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutAsAdmin"/> + + <!-- Run reindex --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <deleteData createDataKey="createBundleProductCreateBundleProduct" stepKey="deleteDynamicBundleProduct"/> + <deleteData createDataKey="simpleProduct1CreateBundleProduct" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2CreateBundleProduct" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createSubCategoryCreateBundleProduct" stepKey="deleteSubCategory"/> + </after> + + <!-- Go to storefront product page --> + <amOnPage url="{{StorefrontProductPage.url($$createBundleProductCreateBundleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToBundleProductPage"/> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + + <!--"Drop-down" type option--> + <!-- Check Tier Prices for product 1 --> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionInput('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct1"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionInput('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct1"/> + <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="DropDownTierPriceTextProduct1"/> + <assertContains stepKey="assertDropDownTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">DropDownTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionInput('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct2"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionInput('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct2"/> + <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="dropDownTierPriceTextProduct2"/> + <assertContains stepKey="assertDropDownTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">dropDownTierPriceTextProduct2</actualResult> + </assertContains> + + <!--"Radio Buttons" type option--> + <!-- Check Tier Prices for product 1 --> + <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct1"/> + <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">radioButtonsOptionTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct2"/> + <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">radioButtonsOptionTierPriceTextProduct2</actualResult> + </assertContains> + + <!--"Checkbox" type option--> + <!-- Check Tier Prices for product 1 --> + <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct1"/> + <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">checkBoxOptionTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct2"/> + <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">checkBoxOptionTierPriceTextProduct2</actualResult> + </assertContains> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php new file mode 100644 index 0000000000000..d335554ef373c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Block\DataProviders; + +use Magento\Bundle\Block\DataProviders\OptionPriceRenderer; +use Magento\Catalog\Model\Product; +use Magento\Framework\Pricing\Render; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Framework\View\LayoutInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class to test additional data for bundle options + */ +class OptionPriceRendererTest extends TestCase +{ + /** + * @var LayoutInterface|MockObject + */ + private $layoutMock; + + /** + * @var OptionPriceRenderer + */ + private $renderer; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->layoutMock = $this->createMock( + LayoutInterface::class + ); + + $this->renderer = $objectManager->getObject( + OptionPriceRenderer::class, + ['layout' => $this->layoutMock] + ); + } + + /** + * Test to render Tier price html + */ + public function testRenderTierPrice() + { + $expectedHtml = 'tier price html'; + $expectedArguments = ['zone' => Render::ZONE_ITEM_OPTION]; + + $productMock = $this->createMock(Product::class); + + $priceRenderer = $this->createPartialMock(BlockInterface::class, ['toHtml', 'render']); + $priceRenderer->expects($this->once()) + ->method('render') + ->with('tier_price', $productMock, $expectedArguments) + ->willReturn($expectedHtml); + + $this->layoutMock->method('getBlock') + ->with('product.price.render.default') + ->willReturn($priceRenderer); + + $this->assertEquals( + $expectedHtml, + $this->renderer->renderTierPrice($productMock), + 'Render Tier price is wrong' + ); + } + + /** + * Test to render Tier price html when render block is not exists + */ + public function testRenderTierPriceNotExist() + { + $productMock = $this->createMock(Product::class); + + $this->layoutMock->method('getBlock') + ->with('product.price.render.default') + ->willReturn(false); + + $this->assertEquals( + '', + $this->renderer->renderTierPrice($productMock), + 'Render Tier price is wrong' + ); + } +} diff --git a/app/code/Magento/Bundle/composer.json b/app/code/Magento/Bundle/composer.json index d12d5e715eb3c..d82e4201bbda4 100644 --- a/app/code/Magento/Bundle/composer.json +++ b/app/code/Magento/Bundle/composer.json @@ -26,7 +26,7 @@ "magento/module-sales-rule": "101.0.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js index e56cc6f32d804..49ee253ad1e88 100644 --- a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js +++ b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js @@ -27,7 +27,8 @@ define([ '<% } %>', controlContainer: 'dd', // should be eliminated priceFormat: {}, - isFixedPrice: false + isFixedPrice: false, + optionTierPricesBlocksSelector: '#option-tier-prices-{1} [data-role="selection-tier-prices"]' }; $.widget('mage.priceBundle', { @@ -91,6 +92,8 @@ define([ if (changes) { priceBox.trigger('updatePrice', changes); } + + this._displayTierPriceBlock(bundleOption); this.updateProductSummary(); }, @@ -207,6 +210,35 @@ define([ return this; }, + /** + * Show or hide option tier prices block + * + * @param {Object} optionElement + * @private + */ + _displayTierPriceBlock: function (optionElement) { + var optionType = optionElement.prop('type'), + optionId, + optionValue, + optionTierPricesElements; + + if (optionType === 'select-one') { + optionId = utils.findOptionId(optionElement[0]); + optionValue = optionElement.val() || null; + optionTierPricesElements = $(this.options.optionTierPricesBlocksSelector.replace('{1}', optionId)); + + _.each(optionTierPricesElements, function (tierPriceElement) { + var selectionId = $(tierPriceElement).data('selection-id') + ''; + + if (selectionId === optionValue) { + $(tierPriceElement).show(); + } else { + $(tierPriceElement).hide(); + } + }); + } + }, + /** * Handler to update productSummary box */ @@ -374,8 +406,17 @@ define([ function applyTierPrice(oneItemPrice, qty, optionConfig) { var tiers = optionConfig.tierPrice, magicKey = _.keys(oneItemPrice)[0], + tiersFirstKey = _.keys(optionConfig)[0], lowest = false; + if (!tiers) {//tiers is undefined when options has only one option + tiers = optionConfig[tiersFirstKey].tierPrice; + } + + tiers.sort(function (a, b) {//sorting based on "price_qty" + return a['price_qty'] - b['price_qty']; + }); + _.each(tiers, function (tier, index) { if (tier['price_qty'] > qty) { return; diff --git a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml index 5b8c050e5af54..d12f2e8f6a952 100644 --- a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml +++ b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml @@ -29,10 +29,22 @@ <container name="product.info.bundle.options.top" as="product_info_bundle_options_top"> <block class="Magento\Catalog\Block\Product\View" name="bundle.back.button" as="backButton" before="-" template="Magento_Bundle::catalog/product/view/backbutton.phtml"/> </container> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"/> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Multi" name="product.info.bundle.options.multi" as="multi"/> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"/> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"/> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> </block> </referenceBlock> <referenceBlock name="product.info.form.options"> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml index bda649eb603e6..830d03c826f32 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml @@ -19,6 +19,7 @@ <div class="nested options-list"> <?php if ($block->showSingle()): ?> <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" @@ -38,6 +39,8 @@ <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> <span><?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection) ?></span> + <br/> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> </label> </div> <?php endforeach; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml index 7ea89e8609818..1f33d97227ea3 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml @@ -21,6 +21,7 @@ <div class="nested options-list"> <?php if ($block->showSingle()): ?> <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= (int)$_option->getId() ?> product bundle option" name="bundle_option[<?= (int)$_option->getId() ?>]" @@ -57,6 +58,8 @@ <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> <span><?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection) ?></span> + <br/> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> </label> </div> <?php endforeach; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml index 977daa2b2a446..65d736f5792df 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml @@ -20,6 +20,7 @@ <div class="control"> <?php if ($block->showSingle()): ?> <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" @@ -39,6 +40,15 @@ </option> <?php endforeach; ?> </select> + <div id="option-tier-prices-<?= $block->escapeHtml($_option->getId()) ?>" class="option-tier-prices"> + <?php foreach ($_selections as $_selection): ?> + <div data-role="selection-tier-prices" + data-selection-id="<?= $block->escapeHtml($_selection->getSelectionId()) ?>" + class="selection-tier-prices"> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> + </div> + <?php endforeach; ?> + </div> <?php endif; ?> <div class="nested"> <div class="field qty qty-holder"> diff --git a/app/code/Magento/BundleImportExport/composer.json b/app/code/Magento/BundleImportExport/composer.json index bfb8bd2b663a1..985b83a197369 100644 --- a/app/code/Magento/BundleImportExport/composer.json +++ b/app/code/Magento/BundleImportExport/composer.json @@ -12,7 +12,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CacheInvalidate/composer.json b/app/code/Magento/CacheInvalidate/composer.json index e35efe0cd2e4c..4b9ef4e140f35 100644 --- a/app/code/Magento/CacheInvalidate/composer.json +++ b/app/code/Magento/CacheInvalidate/composer.json @@ -7,7 +7,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Captcha/Controller/Refresh/Index.php b/app/code/Magento/Captcha/Controller/Refresh/Index.php index e89a80646ed8e..3f831606570ca 100644 --- a/app/code/Magento/Captcha/Controller/Refresh/Index.php +++ b/app/code/Magento/Captcha/Controller/Refresh/Index.php @@ -40,10 +40,15 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $formId = $this->_request->getPost('formId'); if (null === $formId) { $params = []; @@ -51,7 +56,7 @@ public function execute() if ($content) { $params = $this->serializer->unserialize($content); } - $formId = isset($params['formId']) ? $params['formId'] : null; + $formId = $params['formId'] ?? null; } $captchaModel = $this->captchaHelper->getCaptcha($formId); $captchaModel->generate(); diff --git a/app/code/Magento/Captcha/Test/Unit/Controller/Refresh/IndexTest.php b/app/code/Magento/Captcha/Test/Unit/Controller/Refresh/IndexTest.php index 99ac2e2d8fccc..ee97c11a58315 100644 --- a/app/code/Magento/Captcha/Test/Unit/Controller/Refresh/IndexTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Controller/Refresh/IndexTest.php @@ -95,6 +95,7 @@ public function testExecute($formId, $callsNumber) $blockMethods = ['setFormId', 'setIsAjax', 'toHtml']; $blockMock = $this->createPartialMock(\Magento\Captcha\Block\Captcha::class, $blockMethods); + $this->requestMock->expects($this->once())->method('isPost')->willReturn(true); $this->requestMock->expects($this->any())->method('getPost')->with('formId')->will($this->returnValue($formId)); $this->requestMock->expects($this->exactly($callsNumber))->method('getContent') ->will($this->returnValue(json_encode($content))); diff --git a/app/code/Magento/Captcha/composer.json b/app/code/Magento/Captcha/composer.json index 09104f37814ee..471c4f976a300 100644 --- a/app/code/Magento/Captcha/composer.json +++ b/app/code/Magento/Captcha/composer.json @@ -13,7 +13,7 @@ "zendframework/zend-session": "^2.7.3" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php index b65cdafbe26f4..cf5e49c954b43 100644 --- a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php @@ -43,7 +43,7 @@ public function setParentId($parentId); /** * Get category name * - * @return string + * @return string|null */ public function getName(); diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php index 20411a4c4d767..2eef1188e3910 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php @@ -27,7 +27,8 @@ public function getButtonData() return [ 'id' => 'delete', 'label' => __('Delete'), - 'on_click' => "categoryDelete('" . $this->getDeleteUrl() . "')", + 'on_click' => "deleteConfirm('" .__('Are you sure you want to delete this category?') ."', '" + . $this->getDeleteUrl() . "', {data: {}})", 'class' => 'delete', 'sort_order' => 10 ]; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php index ed615b41644e2..a7bb242daf86f 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php @@ -73,7 +73,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -82,7 +82,7 @@ protected function _construct() } /** - * @return $this + * @inheritdoc */ protected function _prepareLayout() { @@ -182,6 +182,8 @@ public function getSuggestedCategoriesJson($namePart) } /** + * Get add root button html + * * @return string */ public function getAddRootButtonHtml() @@ -190,6 +192,8 @@ public function getAddRootButtonHtml() } /** + * Get add sub button html + * * @return string */ public function getAddSubButtonHtml() @@ -198,6 +202,8 @@ public function getAddSubButtonHtml() } /** + * Get expand button html + * * @return string */ public function getExpandButtonHtml() @@ -206,6 +212,8 @@ public function getExpandButtonHtml() } /** + * Get collapse button html + * * @return string */ public function getCollapseButtonHtml() @@ -214,6 +222,8 @@ public function getCollapseButtonHtml() } /** + * Get store switcher + * * @return string */ public function getStoreSwitcherHtml() @@ -222,6 +232,8 @@ public function getStoreSwitcherHtml() } /** + * Get loader tree url + * * @param bool|null $expanded * @return string */ @@ -235,6 +247,8 @@ public function getLoadTreeUrl($expanded = null) } /** + * Get nodes url + * * @return string */ public function getNodesUrl() @@ -243,6 +257,8 @@ public function getNodesUrl() } /** + * Get switcher tree url + * * @return string */ public function getSwitchTreeUrl() @@ -254,6 +270,8 @@ public function getSwitchTreeUrl() } /** + * Get is was expanded + * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -263,7 +281,10 @@ public function getIsWasExpanded() } /** + * Get move url + * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getMoveUrl() { @@ -271,6 +292,8 @@ public function getMoveUrl() } /** + * Get tree + * * @param mixed|null $parenNodeCategory * @return array */ @@ -282,6 +305,8 @@ public function getTree($parenNodeCategory = null) } /** + * Get tree json + * * @param mixed|null $parenNodeCategory * @return string */ @@ -367,7 +392,7 @@ protected function _getNodeJson($node, $level = 0) } } - if ($isParent || $node->getLevel() < 2) { + if ($isParent || $node->getLevel() < 1) { $item['expanded'] = true; } @@ -390,6 +415,8 @@ public function buildNodeName($node) } /** + * Is category movable + * * @param Node|array $node * @return bool */ @@ -403,6 +430,8 @@ protected function _isCategoryMoveable($node) } /** + * Is parent selected category + * * @param Node|array $node * @return bool */ @@ -422,6 +451,7 @@ protected function _isParentSelectedCategory($node) * Check if page loaded by outside link to category edit * * @return boolean + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function isClearEdit() { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php index 6ab039aa27849..695ea6a7288e3 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php @@ -91,7 +91,23 @@ protected function _construct() if (!$entityAttribute || !$entityAttribute->getIsUserDefined()) { $this->buttonList->remove('delete'); } else { - $this->buttonList->update('delete', 'label', __('Delete Attribute')); + $this->buttonList->update( + 'delete', + 'onclick', + sprintf( + "deleteConfirm('%s','%s', %s)", + __('Are you sure you want to do this?'), + $this->getDeleteUrl(), + json_encode( + [ + 'action' => '', + 'data' => [ + 'form_key' => $this->getFormKey() + ] + ] + ) + ) + ); } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php index 1b188de40710f..2cd27f2785af2 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php @@ -140,7 +140,7 @@ protected function _prepareLayout() ) . '\', \'' . $this->getUrl( 'catalog/*/delete', ['id' => $setId] - ) . '\')', + ) . '\', {data: {}})', 'class' => 'delete' ] ); diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php index 4aebd521fe60d..964872b6e51bd 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php @@ -70,11 +70,11 @@ public function getFieldSuffix() * Retrieve current store id * * @return int + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getStoreId() { - $storeId = $this->getRequest()->getParam('store'); - return (int)$storeId; + return (int)$this->getRequest()->getParam('store'); } /** @@ -99,6 +99,8 @@ public function getTabLabel() } /** + * Return Tab title. + * * @return \Magento\Framework\Phrase */ public function getTabTitle() @@ -107,7 +109,7 @@ public function getTabTitle() } /** - * @return bool + * @inheritdoc */ public function canShowTab() { @@ -115,7 +117,7 @@ public function canShowTab() } /** - * @return bool + * @inheritdoc */ public function isHidden() { @@ -123,6 +125,8 @@ public function isHidden() } /** + * Get availability status. + * * @param string $fieldName * @return bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index 8494b690bad9f..69c8b78b017d2 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -97,7 +97,7 @@ public function getAdditionalData(array $excludeAttr = []) if (is_string($value) && strlen(trim($value))) { $data[$attribute->getAttributeCode()] = [ - 'label' => __($attribute->getStoreLabel()), + 'label' => $attribute->getStoreLabel(), 'value' => $value, 'code' => $attribute->getAttributeCode(), ]; diff --git a/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php new file mode 100644 index 0000000000000..7790785133ddf --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Block\Product\View; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Catalog\Block\Product\Context; +use Magento\Framework\Stdlib\ArrayUtils; + +class GalleryOptions extends AbstractView implements ArgumentInterface +{ + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var Gallery + */ + private $gallery; + + /** + * @param Context $context + * @param ArrayUtils $arrayUtils + * @param Json $jsonSerializer + * @param Gallery $gallery + * @param array $data + */ + public function __construct( + Context $context, + ArrayUtils $arrayUtils, + Json $jsonSerializer, + Gallery $gallery, + array $data = [] + ) { + $this->gallery = $gallery; + $this->jsonSerializer = $jsonSerializer; + parent::__construct($context, $arrayUtils, $data); + } + + /** + * Retrieve gallery options in JSON format + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ElseExpression) + */ + public function getOptionsJson() + { + $optionItems = null; + + //Special case for gallery/nav which can be the string "thumbs/false/dots" + if (is_bool($this->getVar("gallery/nav"))) { + $optionItems['nav'] = $this->getVar("gallery/nav") ? 'true' : 'false'; + } else { + $optionItems['nav'] = $this->escapeHtml($this->getVar("gallery/nav")); + } + + $optionItems['loop'] = $this->getVar("gallery/loop"); + $optionItems['keyboard'] = $this->getVar("gallery/keyboard"); + $optionItems['arrows'] = $this->getVar("gallery/arrows"); + $optionItems['allowfullscreen'] = $this->getVar("gallery/allowfullscreen"); + $optionItems['showCaption'] = $this->getVar("gallery/caption"); + $optionItems['width'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_medium', 'width') + ); + $optionItems['thumbwidth'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_small', 'width') + ); + + if ($this->gallery->getImageAttribute('product_page_image_small', 'height') || + $this->gallery->getImageAttribute('product_page_image_small', 'width')) { + $optionItems['thumbheight'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_small', 'height') ?: + $this->gallery->getImageAttribute('product_page_image_small', 'width') + ); + } + + if ($this->gallery->getImageAttribute('product_page_image_medium', 'height') || + $this->gallery->getImageAttribute('product_page_image_medium', 'width')) { + $optionItems['height'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_medium', 'height') ?: + $this->gallery->getImageAttribute('product_page_image_medium', 'width') + ); + } + + if ($this->getVar("gallery/transition/duration")) { + $optionItems['transitionduration'] = + (int)$this->escapeHtml($this->getVar("gallery/transition/duration")); + } + + $optionItems['transition'] = $this->escapeHtml($this->getVar("gallery/transition/effect")); + $optionItems['navarrows'] = $this->getVar("gallery/navarrows"); + $optionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/navtype")); + $optionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/navdir")); + + if ($this->getVar("gallery/thumbmargin")) { + $optionItems['thumbmargin'] = (int)$this->escapeHtml($this->getVar("gallery/thumbmargin")); + } + + return $this->jsonSerializer->serialize($optionItems); + } + + /** + * Retrieve gallery fullscreen options in JSON format + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ElseExpression) + */ + public function getFSOptionsJson() + { + $fsOptionItems = null; + + //Special case for gallery/nav which can be the string "thumbs/false/dots" + if (is_bool($this->getVar("gallery/fullscreen/nav"))) { + $fsOptionItems['nav'] = $this->getVar("gallery/fullscreen/nav") ? 'true' : 'false'; + } else { + $fsOptionItems['nav'] = $this->escapeHtml($this->getVar("gallery/fullscreen/nav")); + } + + $fsOptionItems['loop'] = $this->getVar("gallery/fullscreen/loop"); + $fsOptionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navdir")); + $fsOptionItems['navarrows'] = $this->getVar("gallery/fullscreen/navarrows"); + $fsOptionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navtype")); + $fsOptionItems['arrows'] = $this->getVar("gallery/fullscreen/arrows"); + $fsOptionItems['showCaption'] = $this->getVar("gallery/fullscreen/caption"); + + if ($this->getVar("gallery/fullscreen/transition/duration")) { + $fsOptionItems['transitionduration'] = (int)$this->escapeHtml( + $this->getVar("gallery/fullscreen/transition/duration") + ); + } + + $fsOptionItems['transition'] = $this->escapeHtml($this->getVar("gallery/fullscreen/transition/effect")); + + if ($this->getVar("gallery/fullscreen/keyboard")) { + $fsOptionItems['keyboard'] = $this->getVar("gallery/fullscreen/keyboard"); + } + + if ($this->getVar("gallery/fullscreen/thumbmargin")) { + $fsOptionItems['thumbmargin'] = + (int)$this->escapeHtml($this->getVar("gallery/fullscreen/thumbmargin")); + } + + return $this->jsonSerializer->serialize($fsOptionItems); + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php index 7df9b972e1501..52de7939d7eb2 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php @@ -3,8 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Block\Product\View\Options\Type; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Block\Product\View\Options\Type\Select\CheckableFactory; +use Magento\Catalog\Block\Product\View\Options\Type\Select\MultipleFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Pricing\Helper\Data; +use Magento\Catalog\Helper\Data as CatalogHelper; + /** * Product options text type block * @@ -13,169 +22,64 @@ */ class Select extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions { + /** + * @var CheckableFactory + */ + private $checkableFactory; + + /** + * @var MultipleFactory + */ + private $multipleFactory; + + /** + * Select constructor. + * @param Context $context + * @param Data $pricingHelper + * @param CatalogHelper $catalogData + * @param array $data + * @param CheckableFactory|null $checkableFactory + * @param MultipleFactory|null $multipleFactory + */ + public function __construct( + Context $context, + Data $pricingHelper, + CatalogHelper $catalogData, + array $data = [], + CheckableFactory $checkableFactory = null, + MultipleFactory $multipleFactory = null + ) { + parent::__construct($context, $pricingHelper, $catalogData, $data); + $this->checkableFactory = $checkableFactory ?: ObjectManager::getInstance()->get(CheckableFactory::class); + $this->multipleFactory = $multipleFactory ?: ObjectManager::getInstance()->get(MultipleFactory::class); + } + /** * Return html for control element * * @return string - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getValuesHtml() { - $_option = $this->getOption(); - $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $_option->getId()); - $store = $this->getProduct()->getStore(); - - $this->setSkipJsReloadPrice(1); - // Remove inline prototype onclick and onchange events + $option = $this->getOption(); + $optionType = $option->getType(); - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN || - $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE + if ($optionType === ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN || + $optionType === ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE ) { - $require = $_option->getIsRequire() ? ' required' : ''; - $extraParams = ''; - $select = $this->getLayout()->createBlock( - \Magento\Framework\View\Element\Html\Select::class - )->setData( - [ - 'id' => 'select_' . $_option->getId(), - 'class' => $require . ' product-custom-option admin__control-select' - ] - ); - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) { - $select->setName('options[' . $_option->getId() . ']')->addOption('', __('-- Please Select --')); - } else { - $select->setName('options[' . $_option->getId() . '][]'); - $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); - } - foreach ($_option->getValues() as $_value) { - $priceStr = $this->_formatPrice( - [ - 'is_percent' => $_value->getPriceType() == 'percent', - 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), - ], - false - ); - $select->addOption( - $_value->getOptionTypeId(), - $_value->getTitle() . ' ' . strip_tags($priceStr) . '', - ['price' => $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false)] - ); - } - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE) { - $extraParams = ' multiple="multiple"'; - } - if (!$this->getSkipJsReloadPrice()) { - $extraParams .= ' onchange="opConfig.reloadPrice()"'; - } - $extraParams .= ' data-selector="' . $select->getName() . '"'; - $select->setExtraParams($extraParams); - - if ($configValue) { - $select->setValue($configValue); - } - - return $select->getHtml(); + $optionBlock = $this->multipleFactory->create(); } - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || - $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX + if ($optionType === ProductCustomOptionInterface::OPTION_TYPE_RADIO || + $optionType === ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX ) { - $selectHtml = '<div class="options-list nested" id="options-' . $_option->getId() . '-list">'; - $require = $_option->getIsRequire() ? ' required' : ''; - $arraySign = ''; - switch ($_option->getType()) { - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO: - $type = 'radio'; - $class = 'radio admin__control-radio'; - if (!$_option->getIsRequire()) { - $selectHtml .= '<div class="field choice admin__field admin__field-option">' . - '<input type="radio" id="options_' . - $_option->getId() . - '" class="' . - $class . - ' product-custom-option" name="options[' . - $_option->getId() . - ']"' . - ' data-selector="options[' . $_option->getId() . ']"' . - ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' value="" checked="checked" /><label class="label admin__field-label" for="options_' . - $_option->getId() . - '"><span>' . - __('None') . '</span></label></div>'; - } - break; - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX: - $type = 'checkbox'; - $class = 'checkbox admin__control-checkbox'; - $arraySign = '[]'; - break; - } - $count = 1; - foreach ($_option->getValues() as $_value) { - $count++; - - $priceStr = $this->_formatPrice( - [ - 'is_percent' => $_value->getPriceType() == 'percent', - 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), - ] - ); - - $htmlValue = $_value->getOptionTypeId(); - if ($arraySign) { - $checked = is_array($configValue) && in_array($htmlValue, $configValue) ? 'checked' : ''; - } else { - $checked = $configValue == $htmlValue ? 'checked' : ''; - } - - $dataSelector = 'options[' . $_option->getId() . ']'; - if ($arraySign) { - $dataSelector .= '[' . $htmlValue . ']'; - } - - $selectHtml .= '<div class="field choice admin__field admin__field-option' . - $require . - '">' . - '<input type="' . - $type . - '" class="' . - $class . - ' ' . - $require . - ' product-custom-option"' . - ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' name="options[' . - $_option->getId() . - ']' . - $arraySign . - '" id="options_' . - $_option->getId() . - '_' . - $count . - '" value="' . - $htmlValue . - '" ' . - $checked . - ' data-selector="' . $dataSelector . '"' . - ' price="' . - $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false) . - '" />' . - '<label class="label admin__field-label" for="options_' . - $_option->getId() . - '_' . - $count . - '"><span>' . - $_value->getTitle() . - '</span> ' . - $priceStr . - '</label>'; - $selectHtml .= '</div>'; - } - $selectHtml .= '</div>'; - - return $selectHtml; + $optionBlock = $this->checkableFactory->create(); } + + return $optionBlock + ->setOption($option) + ->setProduct($this->getProduct()) + ->setSkipJsReloadPrice(1) + ->_toHtml(); } } diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php new file mode 100644 index 0000000000000..2b000b1e5105d --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Block\Product\View\Options\Type\Select; + +use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface; +use Magento\Catalog\Block\Product\View\Options\AbstractOptions; + +/** + * Represent necessary logic for checkbox and radio button option type + */ +class Checkable extends AbstractOptions +{ + protected $_template = 'Magento_Catalog::product/composite/fieldset/options/view/checkable.phtml'; + + /** + * @param $value + * @return string + */ + public function formatPrice(ProductCustomOptionValuesInterface $value) : string + { + + return parent::_formatPrice( + [ + 'is_percent' => $value->getPriceType() === 'percent', + 'pricing_value' => $value->getPrice($value->getPriceType() === 'percent') + ] + ); + } + + /** + * @param $value + * @return float + */ + public function getCurrencyByStore(ProductCustomOptionValuesInterface $value) : float + { + return $this->pricingHelper->currencyByStore( + $value->getPrice(true), + $this->getProduct()->getStore(), + false + ); + } + + /** + * @param $option + * @return string|array|null + */ + public function getPreconfiguredValue($option) + { + return $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php new file mode 100644 index 0000000000000..20164b0622a6d --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Block\Product\View\Options\Type\Select; + +use Magento\Catalog\Block\Product\View\Options\AbstractOptions; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\View\Element\Html\Select; + +/** + * Class represents necessary logic for dropdown and multiselect option types + */ +class Multiple extends AbstractOptions +{ + /** + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function _toHtml() + { + $option = $this->getOption(); + $optionType = $option->getType(); + $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + $require = $option->getIsRequire() ? ' required' : ''; + $extraParams = ''; + /** @var Select $select */ + $select = $this->getLayout()->createBlock( + Select::class + )->setData( + [ + 'id' => 'select_' . $option->getId(), + 'class' => $require . ' product-custom-option admin__control-select' + ] + ); + + $select = $this->insertSelectOption($select, $option); + $select = $this->processSelectOption($select, $option); + + if ($optionType === ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE) { + $extraParams = ' multiple="multiple"'; + } + + if (!$this->getSkipJsReloadPrice()) { + $extraParams .= ' onchange="opConfig.reloadPrice()"'; + } + + $extraParams .= ' data-selector="' . $select->getName() . '"'; + $select->setExtraParams($extraParams); + + if ($configValue) { + $select->setValue($configValue); + } + + return $select->getHtml(); + } + + /** + * @param Select $select + * @param Option $option + * @return Select + */ + private function insertSelectOption(Select $select, Option $option) : Select + { + $require = $option->getIsRequire() ? ' required' : ''; + + if ($option->getType() === ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) { + $select->setName('options[' . $option->getId() . ']')->addOption('', __('-- Please Select --')); + } else { + $select->setName('options[' . $option->getId() . '][]'); + $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); + } + + return $select; + } + + /** + * @param Select $select + * @param Option $option + * @return Select + */ + private function processSelectOption(Select $select, Option $option) : Select + { + $store = $this->getProduct()->getStore(); + + foreach ($option->getValues() as $_value) { + $isPercentPriceType = $_value->getPriceType() === 'percent'; + $priceStr = $this->_formatPrice( + [ + 'is_percent' => $isPercentPriceType, + 'pricing_value' => $_value->getPrice($isPercentPriceType) + ], + false + ); + + $select->addOption( + $_value->getOptionTypeId(), + $_value->getTitle() . ' ' . strip_tags($priceStr) . '', + [ + 'price' => $this->pricingHelper->currencyByStore( + $_value->getPrice(true), + $store, + false + ) + ] + ); + } + + return $select; + } +} diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php index b8865f2de8d1e..3969d8193817b 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php @@ -37,9 +37,14 @@ public function __construct( * Get tree node (Ajax version) * * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + if ($this->getRequest()->getParam('expand_all')) { $this->_objectManager->get(\Magento\Backend\Model\Auth\Session::class)->setIsTreeWasExpanded(true); } else { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php index 0a54475b15f9c..4f14fb58487c1 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php @@ -29,9 +29,14 @@ public function __construct( * Delete category action * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php index df2c80eda141c..02ddb162aff3a 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php @@ -26,7 +26,7 @@ class Move extends \Magento\Catalog\Controller\Adminhtml\Category /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory - * @param \Magento\Framework\View\LayoutFactory $layoutFactory, + * @param \Magento\Framework\View\LayoutFactory $layoutFactory * @param \Psr\Log\LoggerInterface $logger */ public function __construct( @@ -45,16 +45,17 @@ public function __construct( * Move category action * * @return \Magento\Framework\Controller\Result\Raw + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { - /** - * New parent category identifier - */ + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + + /** New parent category identifier */ $parentNodeId = $this->getRequest()->getPost('pid', false); - /** - * Category id after which we have put our category - */ + /** Category id after which we have put our category */ $prevNodeId = $this->getRequest()->getPost('aid', false); /** @var $block \Magento\Framework\View\Element\Messages */ diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php index 0fbf9054ef1bd..492288308a95c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; use Magento\Backend\App\Action; +use Magento\Framework\Exception\NotFoundException; /** * Class Save @@ -81,12 +82,17 @@ public function __construct( * Update product attributes * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + if (!$this->_validateProducts()) { return $this->resultRedirectFactory->create()->setPath('catalog/product/', ['_current' => true]); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php index bef6aee0e2afd..f4b55f081afc4 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php @@ -6,13 +6,20 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; +use Magento\Framework\Exception\NotFoundException; + class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $id = $this->getRequest()->getParam('attribute_id'); $resultRedirect = $this->resultRedirectFactory->create(); if ($id) { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php index 3568d15b8048d..da17ad3bb80b2 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -29,6 +29,7 @@ use Magento\Framework\Registry; use Magento\Framework\View\LayoutFactory; use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\Exception\NotFoundException; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -117,6 +118,7 @@ public function __construct( /** * @inheritdoc + * @throws NotFoundException * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -124,6 +126,10 @@ public function __construct( */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { $optionData = $this->formDataSerializer->unserialize( $this->getRequest()->getParam('serialized_options', '[]') diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php index 125406061aed7..448de260f2eed 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php @@ -15,6 +15,9 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type as ProductTypes; +/** + * Build a product based on a request. + */ class Builder { /** @@ -92,6 +95,9 @@ public function build(RequestInterface $request) if ($productId) { try { $product = $this->productRepository->getById($productId, true, $storeId); + if ($attributeSetId) { + $product->setAttributeSetId($attributeSetId); + } } catch (\Exception $e) { $product = $this->createEmptyProduct(ProductTypes::DEFAULT_TYPE, $attributeSetId, $storeId); $this->logger->critical($e); @@ -113,6 +119,8 @@ public function build(RequestInterface $request) } /** + * Create a product with the given properties + * * @param int $typeId * @param int $attributeSetId * @param int $storeId diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php index b6e7e31fb9efd..02b0025a7922a 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php @@ -9,6 +9,7 @@ use Magento\Backend\App\Action; use Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\NotFoundException; use Magento\Ui\Component\MassAction\Filter; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; @@ -77,9 +78,14 @@ public function _validateMassStatus(array $productIds, $status) * Update product(s) status action * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); $productIds = $collection->getAllIds(); $storeId = (int) $this->getRequest()->getParam('store', 0); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index bf0d740fc98fb..7a382d1cb31bc 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -79,11 +79,12 @@ public function __construct( } /** - * Save product action + * Save product action. * * @return \Magento\Backend\Model\View\Result\Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function execute() { @@ -143,6 +144,7 @@ public function execute() if ($redirectBack === 'duplicate') { $product->unsetData('quantity_and_stock_status'); $newProduct = $this->productCopier->copy($product); + $this->checkUniqueAttributes($product); $this->messageManager->addSuccessMessage(__('You duplicated the product.')); } } catch (\Magento\Framework\Exception\LocalizedException $e) { @@ -321,4 +323,25 @@ private function persistMediaData(ProductInterface $product, array $data) return $data; } + + /** + * Check unique attributes and add error to message manager. + * + * @param \Magento\Catalog\Model\Product $product + */ + private function checkUniqueAttributes(\Magento\Catalog\Model\Product $product) + { + $uniqueLabels = []; + foreach ($product->getAttributes() as $attribute) { + if ($attribute->getIsUnique() && $attribute->getIsUserDefined() + && !empty($product->getData($attribute->getAttributeCode())) + ) { + $uniqueLabels[] = $attribute->getDefaultFrontendLabel(); + } + } + if ($uniqueLabels) { + $uniqueLabels = implode('", "', $uniqueLabels); + $this->messageManager->addErrorMessage(__('The value of attribute(s) "%1" must be unique', $uniqueLabels)); + } + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php index f2695311732f0..09d35c4f72de6 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php @@ -6,6 +6,8 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; +use Magento\Framework\Exception\NotFoundException; + class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Set { /** @@ -29,10 +31,15 @@ public function __construct( /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { - $setId = $this->getRequest()->getParam('id'); + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + + $setId = (int)$this->getRequest()->getParam('id'); $resultRedirect = $this->resultRedirectFactory->create(); try { $this->attributeSetRepository->deleteById($setId); @@ -42,6 +49,7 @@ public function execute() $this->messageManager->addErrorMessage(__('We can\'t delete this set right now.')); $resultRedirect->setUrl($this->_redirect->getRedirectUrl($this->getUrl('*'))); } + return $resultRedirect; } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php index eb9cc83125541..24dec0e709122 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php @@ -18,7 +18,7 @@ class Add extends \Magento\Catalog\Controller\Product\Compare public function execute() { $resultRedirect = $this->resultRedirectFactory->create(); - if (!$this->_formKeyValidator->validate($this->getRequest())) { + if (!$this->isActionAllowed()) { return $resultRedirect->setRefererUrl(); } @@ -51,4 +51,12 @@ public function execute() } return $resultRedirect->setRefererOrBaseUrl(); } + + /** + * @return bool + */ + private function isActionAllowed(): bool + { + return $this->getRequest()->isPost() && $this->_formKeyValidator->validate($this->getRequest()); + } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php index 568fbf1d05677..ebbf90e0701ae 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php @@ -17,29 +17,42 @@ class Clear extends \Magento\Catalog\Controller\Product\Compare */ public function execute() { - /** @var \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection $items */ - $items = $this->_itemCollectionFactory->create(); + if ($this->isActionAllowed()) { + /** @var \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection $items */ + $items = $this->_itemCollectionFactory->create(); - if ($this->_customerSession->isLoggedIn()) { - $items->setCustomerId($this->_customerSession->getCustomerId()); - } elseif ($this->_customerId) { - $items->setCustomerId($this->_customerId); - } else { - $items->setVisitorId($this->_customerVisitor->getId()); - } + if ($this->_customerSession->isLoggedIn()) { + $items->setCustomerId($this->_customerSession->getCustomerId()); + } elseif ($this->_customerId) { + $items->setCustomerId($this->_customerId); + } else { + $items->setVisitorId($this->_customerVisitor->getId()); + } - try { - $items->clear(); - $this->messageManager->addSuccessMessage(__('You cleared the comparison list.')); - $this->_objectManager->get(\Magento\Catalog\Helper\Product\Compare::class)->calculate(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { - $this->messageManager->addExceptionMessage($e, __('Something went wrong clearing the comparison list.')); + try { + $items->clear(); + $this->messageManager->addSuccessMessage(__('You cleared the comparison list.')); + $this->_objectManager->get(\Magento\Catalog\Helper\Product\Compare::class)->calculate(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } catch (\Exception $e) { + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong clearing the comparison list.') + ); + } } /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); return $resultRedirect->setRefererOrBaseUrl(); } + + /** + * @return bool + */ + private function isActionAllowed(): bool + { + return $this->getRequest()->isPost() && $this->_formKeyValidator->validate($this->getRequest()); + } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php index 2acbe5ce4d582..36aa0ea1caf9d 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php @@ -18,7 +18,7 @@ class Remove extends \Magento\Catalog\Controller\Product\Compare public function execute() { $productId = (int)$this->getRequest()->getParam('product'); - if ($productId) { + if ($this->isActionAllowed() && $productId) { $storeId = $this->_storeManager->getStore()->getId(); try { $product = $this->productRepository->getById($productId, false, $storeId); @@ -61,4 +61,12 @@ public function execute() return $resultRedirect->setRefererOrBaseUrl(); } } + + /** + * @return bool + */ + private function isActionAllowed(): bool + { + return $this->getRequest()->isPost() && $this->_formKeyValidator->validate($this->getRequest()); + } } diff --git a/app/code/Magento/Catalog/Model/CategoryList.php b/app/code/Magento/Catalog/Model/CategoryList.php index 86692c7d6bc61..cab8e013d9ba1 100644 --- a/app/code/Magento/Catalog/Model/CategoryList.php +++ b/app/code/Magento/Catalog/Model/CategoryList.php @@ -78,8 +78,8 @@ public function getList(SearchCriteriaInterface $searchCriteria) $this->collectionProcessor->process($searchCriteria, $collection); $items = []; - foreach ($collection->getItems() as $category) { - $items[] = $this->categoryRepository->get($category->getId()); + foreach ($collection->getAllIds() as $id) { + $items[] = $this->categoryRepository->get($id); } /** @var CategorySearchResultsInterface $searchResult */ diff --git a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php index e2b0a91574021..d7d342f357519 100644 --- a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php +++ b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php @@ -5,10 +5,14 @@ */ namespace Magento\Catalog\Model\Config\CatalogClone\Media; +use Magento\Framework\Escaper; +use Magento\Framework\App\ObjectManager; + /** * Clone model for media images related config fields * * @SuppressWarnings(PHPMD.LongVariable) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Image extends \Magento\Framework\App\Config\Value { @@ -26,6 +30,11 @@ class Image extends \Magento\Framework\App\Config\Value */ protected $_attributeCollectionFactory; + /** + * @var Escaper + */ + private $escaper; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -36,6 +45,9 @@ class Image extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param Escaper|null $escaper + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -46,8 +58,10 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + Escaper $escaper = null ) { + $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); $this->_attributeCollectionFactory = $attributeCollectionFactory; $this->_eavConfig = $eavConfig; parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); @@ -74,7 +88,7 @@ public function getPrefixes() /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ $prefixes[] = [ 'field' => $attribute->getAttributeCode() . '_', - 'label' => $attribute->getFrontend()->getLabel(), + 'label' => $this->escaper->escapeHtml($attribute->getFrontend()->getLabel()), ]; } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php index f8121b55dbf99..b12ffe1ac1f87 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php @@ -5,31 +5,41 @@ */ namespace Magento\Catalog\Model\Indexer\Category\Product\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Framework\Indexer\BatchSizeManagementInterface; use Magento\Indexer\Model\ProcessManager; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action * - * @package Magento\Catalog\Model\Indexer\Category\Product\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\Indexer\BatchSizeManagementInterface + * @var BatchSizeManagementInterface */ private $batchSizeManagement; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ protected $metadataPool; @@ -52,25 +62,25 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio /** * @param ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\Config $config + * @param StoreManagerInterface $storeManager + * @param Config $config * @param QueryGenerator|null $queryGenerator - * @param \Magento\Framework\Indexer\BatchSizeManagementInterface|null $batchSizeManagement - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool + * @param BatchSizeManagementInterface|null $batchSizeManagement + * @param BatchProviderInterface|null $batchProvider + * @param MetadataPool|null $metadataPool * @param int|null $batchRowsCount * @param ActiveTableSwitcher|null $activeTableSwitcher * @param ProcessManager $processManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Config $config, + ResourceConnection $resource, + StoreManagerInterface $storeManager, + Config $config, QueryGenerator $queryGenerator = null, - \Magento\Framework\Indexer\BatchSizeManagementInterface $batchSizeManagement = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + BatchSizeManagementInterface $batchSizeManagement = null, + BatchProviderInterface $batchProvider = null, + MetadataPool $metadataPool = null, $batchRowsCount = null, ActiveTableSwitcher $activeTableSwitcher = null, ProcessManager $processManager = null @@ -81,15 +91,15 @@ public function __construct( $config, $queryGenerator ); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $objectManager = ObjectManager::getInstance(); $this->batchSizeManagement = $batchSizeManagement ?: $objectManager->get( - \Magento\Framework\Indexer\BatchSizeManagementInterface::class + BatchSizeManagementInterface::class ); $this->batchProvider = $batchProvider ?: $objectManager->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->metadataPool = $metadataPool ?: $objectManager->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchRowsCount = $batchRowsCount; $this->activeTableSwitcher = $activeTableSwitcher ?: $objectManager->get(ActiveTableSwitcher::class); @@ -97,41 +107,45 @@ public function __construct( } /** + * Create the store tables + * * @return void */ private function createTables() { foreach ($this->storeManager->getStores() as $store) { - $this->tableMaintainer->createTablesForStore($store->getId()); + $this->tableMaintainer->createTablesForStore((int)$store->getId()); } } /** + * Truncates the replica tables + * * @return void */ private function clearReplicaTables() { foreach ($this->storeManager->getStores() as $store) { - $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())); } } /** + * Switches the active table + * * @return void */ private function switchTables() { $tablesToSwitch = []; foreach ($this->storeManager->getStores() as $store) { - $tablesToSwitch[] = $this->tableMaintainer->getMainTable($store->getId()); + $tablesToSwitch[] = $this->tableMaintainer->getMainTable((int)$store->getId()); } $this->activeTableSwitcher->switchTable($this->connection, $tablesToSwitch); } /** - * Refresh entities index - * - * @return $this + * @inheritdoc */ public function execute() { @@ -139,6 +153,7 @@ public function execute() $this->clearReplicaTables(); $this->reindex(); $this->switchTables(); + return $this; } @@ -165,7 +180,7 @@ protected function reindex() /** * Execute indexation by store * - * @param \Magento\Store\Model\Store $store + * @param Store $store */ private function reindexStore($store) { @@ -177,31 +192,31 @@ private function reindexStore($store) /** * Publish data from tmp to replica table * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ private function publishData($store) { - $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable($store->getId())); + $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable((int)$store->getId())); $columns = array_keys( - $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable($store->getId())) + $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())) ); - $tableName = $this->tableMaintainer->getMainReplicaTable($store->getId()); + $tableName = $this->tableMaintainer->getMainReplicaTable((int)$store->getId()); $this->connection->query( $this->connection->insertFromSelect( $select, $tableName, $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); } /** - * {@inheritdoc} + * @inheritdoc */ - protected function reindexRootCategory(\Magento\Store\Model\Store $store) + protected function reindexRootCategory(Store $store) { if ($this->isIndexRootCategoryNeeded()) { $this->reindexCategoriesBySelect($this->getAllProducts($store), 'cp.entity_id IN (?)', $store); @@ -211,10 +226,10 @@ protected function reindexRootCategory(\Magento\Store\Model\Store $store) /** * Reindex products of anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexAnchorCategories(Store $store) { $this->reindexCategoriesBySelect($this->getAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } @@ -222,10 +237,10 @@ protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) /** * Reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexNonAnchorCategories(Store $store) { $this->reindexCategoriesBySelect($this->getNonAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } @@ -233,40 +248,42 @@ protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) /** * Reindex categories using given SQL select and condition. * - * @param \Magento\Framework\DB\Select $basicSelect + * @param Select $basicSelect * @param string $whereCondition - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSelect, $whereCondition, $store) + private function reindexCategoriesBySelect(Select $basicSelect, $whereCondition, $store) { - $this->tableMaintainer->createMainTmpTable($store->getId()); + $this->tableMaintainer->createMainTmpTable((int)$store->getId()); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); $columns = array_keys( - $this->connection->describeTable($this->tableMaintainer->getMainTmpTable($store->getId())) + $this->connection->describeTable($this->tableMaintainer->getMainTmpTable((int)$store->getId())) ); $this->batchSizeManagement->ensureBatchSize($this->connection, $this->batchRowsCount); - $batches = $this->batchProvider->getBatches( - $this->connection, - $entityMetadata->getEntityTable(), + + $select = $this->connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->prepareSelectsByRange( + $select, $entityMetadata->getIdentifierField(), - $this->batchRowsCount + (int)$this->batchRowsCount ); - foreach ($batches as $batch) { - $this->connection->delete($this->tableMaintainer->getMainTmpTable($store->getId())); + + foreach ($batchQueries as $query) { + $this->connection->delete($this->tableMaintainer->getMainTmpTable((int)$store->getId())); + $entityIds = $this->connection->fetchCol($query); $resultSelect = clone $basicSelect; - $select = $this->connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($this->connection, $select, $batch); $resultSelect->where($whereCondition, $entityIds); $this->connection->query( $this->connection->insertFromSelect( $resultSelect, - $this->tableMaintainer->getMainTmpTable($store->getId()), + $this->tableMaintainer->getMainTmpTable((int)$store->getId()), $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); $this->publishData($store); diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php index 802176092d147..b9ca4f342b45b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php @@ -7,26 +7,41 @@ namespace Magento\Catalog\Model\Indexer\Product\Eav\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIteratorInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Store\Model\ScopeInterface; /** * Class Full reindex action + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; @@ -36,44 +51,54 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction private $activeTableSwitcher; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ private $scopeConfig; /** - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param DecimalFactory $eavDecimalFactory + * @param SourceFactory $eavSourceFactory + * @param MetadataPool|null $metadataPool + * @param BatchProviderInterface|null $batchProvider + * @param BatchSizeCalculator $batchSizeCalculator * @param ActiveTableSwitcher|null $activeTableSwitcher - * @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig + * @param ScopeConfigInterface|null $scopeConfig + * @param QueryGenerator|null $batchQueryGenerator */ public function __construct( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator = null, + DecimalFactory $eavDecimalFactory, + SourceFactory $eavSourceFactory, + MetadataPool $metadataPool = null, + BatchProviderInterface $batchProvider = null, + BatchSizeCalculator $batchSizeCalculator = null, ActiveTableSwitcher $activeTableSwitcher = null, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + QueryGenerator $batchQueryGenerator = null ) { - $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\App\Config\ScopeConfigInterface::class + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get( + ScopeConfigInterface::class ); parent::__construct($eavDecimalFactory, $eavSourceFactory, $scopeConfig); - $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( + MetadataPool::class ); - $this->batchProvider = $batchProvider ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( + BatchProviderInterface::class ); - $this->batchSizeCalculator = $batchSizeCalculator ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class + $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( + BatchSizeCalculator::class ); - $this->activeTableSwitcher = $activeTableSwitcher ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( ActiveTableSwitcher::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get( + QueryGenerator::class + ); } /** @@ -81,7 +106,7 @@ public function __construct( * * @param array|int|null $ids * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($ids = null) @@ -94,20 +119,21 @@ public function execute($ids = null) $connection = $indexer->getConnection(); $mainTable = $this->activeTableSwitcher->getAdditionalTableName($indexer->getMainTable()); $connection->truncateTable($mainTable); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), - $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName) + $select, + $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName), + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR ); - foreach ($batches as $batch) { - /** @var \Magento\Framework\DB\Select $select */ - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); + foreach ($batchQueries as $query) { + $entityIds = $connection->fetchCol($query); if (!empty($entityIds)) { $indexer->reindexEntities($this->processRelations($indexer, $entityIds, true)); $this->syncData($indexer, $mainTable); @@ -116,7 +142,7 @@ public function execute($ids = null) $this->activeTableSwitcher->switchTable($indexer->getConnection(), [$indexer->getMainTable()]); } } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); + throw new LocalizedException(__($e->getMessage()), $e); } } @@ -136,7 +162,7 @@ protected function syncData($indexer, $destinationTable, $ids = null) $select, $destinationTable, $targetColumns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ); $connection->query($query); $connection->commit(); @@ -155,7 +181,7 @@ private function isEavIndexerEnabled(): bool { $eavIndexerStatus = $this->scopeConfig->getValue( self::ENABLE_EAV_INDEXER, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); return (bool)$eavIndexerStatus; diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php index 1a75751570658..79eeb3cc3225d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php @@ -3,41 +3,64 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Product\Price\Action; +use Magento\Catalog\Model\Indexer\Product\Price\AbstractAction; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIterator; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; use Magento\Framework\Indexer\DimensionalIndexerInterface; use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Indexer\Model\ProcessManager; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var ActiveTableSwitcher */ private $activeTableSwitcher; @@ -47,54 +70,61 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction private $productMetaDataCached; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory + * @var DimensionCollectionFactory */ private $dimensionCollectionFactory; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer + * @var TableMaintainer */ private $dimensionTableMaintainer; /** - * @var \Magento\Indexer\Model\ProcessManager + * @var ProcessManager */ private $processManager; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $config - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Framework\Stdlib\DateTime $dateTime - * @param \Magento\Catalog\Model\Product\Type $catalogProductType - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator|null $batchSizeCalculator - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher - * @param \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory|null $dimensionCollectionFactory - * @param \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer|null $dimensionTableMaintainer - * @param \Magento\Indexer\Model\ProcessManager $processManager + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param ScopeConfigInterface $config + * @param StoreManagerInterface $storeManager + * @param CurrencyFactory $currencyFactory + * @param TimezoneInterface $localeDate + * @param DateTime $dateTime + * @param Type $catalogProductType + * @param Factory $indexerPriceFactory + * @param DefaultPrice $defaultIndexerResource + * @param MetadataPool|null $metadataPool + * @param BatchSizeCalculator|null $batchSizeCalculator + * @param BatchProviderInterface|null $batchProvider + * @param ActiveTableSwitcher|null $activeTableSwitcher + * @param DimensionCollectionFactory|null $dimensionCollectionFactory + * @param TableMaintainer|null $dimensionTableMaintainer + * @param ProcessManager $processManager + * @param QueryGenerator|null $batchQueryGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $config, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Directory\Model\CurrencyFactory $currencyFactory, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Catalog\Model\Product\Type $catalogProductType, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator $batchSizeCalculator = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null, - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory = null, - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $dimensionTableMaintainer = null, - \Magento\Indexer\Model\ProcessManager $processManager = null + ScopeConfigInterface $config, + StoreManagerInterface $storeManager, + CurrencyFactory $currencyFactory, + TimezoneInterface $localeDate, + DateTime $dateTime, + Type $catalogProductType, + Factory $indexerPriceFactory, + DefaultPrice $defaultIndexerResource, + MetadataPool $metadataPool = null, + BatchSizeCalculator $batchSizeCalculator = null, + BatchProviderInterface $batchProvider = null, + ActiveTableSwitcher $activeTableSwitcher = null, + DimensionCollectionFactory $dimensionCollectionFactory = null, + TableMaintainer $dimensionTableMaintainer = null, + ProcessManager $processManager = null, + QueryGenerator $batchQueryGenerator = null ) { parent::__construct( $config, @@ -107,26 +137,27 @@ public function __construct( $defaultIndexerResource ); $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator::class + BatchSizeCalculator::class ); $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class + ActiveTableSwitcher::class ); $this->dimensionCollectionFactory = $dimensionCollectionFactory ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class + DimensionCollectionFactory::class ); $this->dimensionTableMaintainer = $dimensionTableMaintainer ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class + TableMaintainer::class ); $this->processManager = $processManager ?: ObjectManager::getInstance()->get( - \Magento\Indexer\Model\ProcessManager::class + ProcessManager::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?? ObjectManager::getInstance()->get(QueryGenerator::class); } /** @@ -143,7 +174,7 @@ public function execute($ids = null) //Prepare indexer tables before full reindex $this->prepareTables(); - /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $indexer */ + /** @var DefaultPrice $indexer */ foreach ($this->getTypeIndexers(true) as $typeId => $priceIndexer) { if ($priceIndexer instanceof DimensionalIndexerInterface) { //New price reindex mechanism @@ -207,7 +238,7 @@ private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $p $userFunctions = []; foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $userFunctions[] = function () use ($priceIndexer, $dimensions, $typeId) { - return $this->reindexByBatches($priceIndexer, $dimensions, $typeId); + $this->reindexByBatches($priceIndexer, $dimensions, $typeId); }; } $this->processManager->execute($userFunctions); @@ -226,7 +257,7 @@ private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $p private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, array $dimensions, string $typeId) { foreach ($this->getBatchesForIndexer($typeId) as $batch) { - $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions, $typeId); + $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions); } } @@ -235,16 +266,19 @@ private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, arr * * @param string $typeId * - * @return \Generator - * @throws \Exception + * @return BatchIterator */ - private function getBatchesForIndexer(string $typeId) + private function getBatchesForIndexer(string $typeId): BatchIterator { $connection = $this->_defaultIndexerResource->getConnection(); - return $this->batchProvider->getBatches( - $connection, - $this->getProductMetaData()->getEntityTable(), - $this->getProductMetaData()->getIdentifierField(), + $entityMetadata = $this->getProductMetaData(); + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + return $this->batchQueryGenerator->generate( + $entityMetadata->getIdentifierField(), + $select, $this->batchSizeCalculator->estimateBatchSize( $connection, $typeId @@ -256,20 +290,18 @@ private function getBatchesForIndexer(string $typeId) * Reindex by batch for new 'Dimensional' price indexer * * @param DimensionalIndexerInterface $priceIndexer - * @param array $batch + * @param Select $batchQuery * @param array $dimensions - * @param string $typeId * * @return void * @throws \Exception */ private function reindexByBatchWithDimensions( DimensionalIndexerInterface $priceIndexer, - array $batch, - array $dimensions, - string $typeId + Select $batchQuery, + array $dimensions ) { - $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); + $entityIds = $this->getEntityIdsFromBatch($batchQuery); if (!empty($entityIds)) { $this->dimensionTableMaintainer->createMainTmpTable($dimensions); @@ -298,7 +330,7 @@ private function reindexByBatchWithDimensions( private function reindexProductType(PriceInterface $priceIndexer, string $typeId) { foreach ($this->getBatchesForIndexer($typeId) as $batch) { - $this->reindexBatch($priceIndexer, $batch, $typeId); + $this->reindexBatch($priceIndexer, $batch); } } @@ -306,15 +338,13 @@ private function reindexProductType(PriceInterface $priceIndexer, string $typeId * Reindex by batch for old price indexer * * @param PriceInterface $priceIndexer - * @param array $batch - * @param string $typeId - * + * @param Select $batch * @return void * @throws \Exception */ - private function reindexBatch(PriceInterface $priceIndexer, array $batch, string $typeId) + private function reindexBatch(PriceInterface $priceIndexer, Select $batch) { - $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); + $entityIds = $this->getEntityIdsFromBatch($batch); if (!empty($entityIds)) { // Temporary table will created if not exists @@ -339,36 +369,22 @@ private function reindexBatch(PriceInterface $priceIndexer, array $batch, string /** * Get Entity Ids from batch * - * @param string $typeId - * @param array $batch - * + * @param Select $batch * @return array - * @throws \Exception */ - private function getEntityIdsFromBatch(string $typeId, array $batch) + private function getEntityIdsFromBatch(Select $batch): array { $connection = $this->_defaultIndexerResource->getConnection(); - // Get entity ids from batch - $select = $connection - ->select() - ->distinct(true) - ->from( - ['e' => $this->getProductMetaData()->getEntityTable()], - $this->getProductMetaData()->getIdentifierField() - ) - ->where('type_id = ?', $typeId); - - return $this->batchProvider->getBatchIds($connection, $select, $batch); + return $connection->fetchCol($batch); } /** * Get product meta data * * @return EntityMetadataInterface - * @throws \Exception */ - private function getProductMetaData() + private function getProductMetaData(): EntityMetadataInterface { if ($this->productMetaDataCached === null) { $this->productMetaDataCached = $this->metadataPool->getMetadata(ProductInterface::class); @@ -381,9 +397,8 @@ private function getProductMetaData() * Get replica table * * @return string - * @throws \Exception */ - private function getReplicaTable() + private function getReplicaTable(): string { return $this->activeTableSwitcher->getAdditionalTableName( $this->_defaultIndexerResource->getMainTable() @@ -417,10 +432,10 @@ private function switchTables() /** * Move data from old price indexer mechanism to new indexer mechanism by dimensions. + * * Used only for backward compatibility * * @param array $dimensions - * * @return void */ private function moveDataFromReplicaTableToReplicaTables(array $dimensions) @@ -455,17 +470,17 @@ private function moveDataFromReplicaTableToReplicaTables(array $dimensions) $select, $replicaTablesByDimension, [], - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); } /** - * @deprecated + * Retrieves the index table that should be used * - * @inheritdoc + * @deprecated */ - protected function getIndexTargetTable() + protected function getIndexTargetTable(): string { return $this->activeTableSwitcher->getAdditionalTableName($this->_defaultIndexerResource->getMainTable()); } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php index 1b5f96baeaf9f..33813d5079215 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php @@ -68,8 +68,10 @@ public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry) $product->setMediaGalleryEntries($existingMediaGalleryEntries); try { $product = $this->productRepository->save($product); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InputException $inputException) { throw $inputException; + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { throw new StateException(__('Cannot save product.')); } @@ -100,7 +102,10 @@ public function update($sku, ProductAttributeMediaGalleryEntryInterface $entry) if ($existingEntry->getId() == $entry->getId()) { $found = true; - if ($entry->getFile()) { + + $file = $entry->getContent(); + + if ($file && $file->getBase64EncodedData() || $entry->getFile()) { $entry->setId(null); } $existingMediaGalleryEntries[$key] = $entry; diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php index bf7bfbe681929..830c1926cc483 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php @@ -191,7 +191,7 @@ public function addImage( $mediaGalleryData = $product->getData($attrCode); $position = 0; - $absoluteFilePath = $this->mediaDirectory->getAbsolutePath($file); + $absoluteFilePath = $this->mediaDirectory->getAbsolutePath($destinationFile); $imageMimeType = $this->mime->getMimeType($absoluteFilePath); $imageContent = $this->mediaDirectory->readFile($absoluteFilePath); $imageBase64 = base64_encode($imageContent); @@ -489,7 +489,7 @@ protected function getNotDuplicatedFilename($fileName, $dispretionPath) /** * Retrieve data for update attribute * - * @param \Magento\Catalog\Model\Product $object + * @param \Magento\Catalog\Model\Product $object * @return array * @since 101.0.0 */ diff --git a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php index d15e8ef5efa55..6eec110e76c10 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Option; use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface as OptionRepository; @@ -58,11 +60,28 @@ public function execute($entity, $arguments = []) } } if ($options) { - foreach ($options as $option) { - $this->optionRepository->save($option); - } + $this->processOptionsSaving($options, $entity->dataHasChangedFor('sku'), $entity->getSku()); } return $entity; } + + /** + * Save custom options + * + * @param array $options + * @param bool $hasChangedSku + * @param string $newSku + * + * @return void + */ + private function processOptionsSaving(array $options, bool $hasChangedSku, string $newSku) + { + foreach ($options as $option) { + if ($hasChangedSku && $option->hasData('product_sku')) { + $option->setProductSku($newSku); + } + $this->optionRepository->save($option); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php index 7a1926cf642ec..5d2490e01dc3a 100644 --- a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php +++ b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php @@ -138,7 +138,9 @@ private function getProductIdsByActions(array $actions) $productIds = []; foreach ($actions as $action) { - $productIds[] = $action['product_id']; + if (isset($action['product_id']) && (int)$action['product_id']) { + $productIds[] = (int)$action['product_id']; + } } return $productIds; @@ -159,33 +161,37 @@ public function syncActions(array $productsData, $typeId) $customerId = $this->session->getCustomerId(); $visitorId = $this->visitor->getId(); $collection = $this->getActionsByType($typeId); - $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); - - /** - * Note that collection is also filtered by visitor id and customer id - * This collection shouldnt be flushed when visitor has products and then login - * It can remove only products for visitor, or only products for customer - * - * ['product_id' => 'added_at'] - * @var ProductFrontendActionInterface $item - */ - foreach ($collection as $item) { - $this->entityManager->delete($item); - } - - foreach ($productsData as $productId => $productData) { - /** @var ProductFrontendActionInterface $action */ - $action = $this->productFrontendActionFactory->create([ - 'data' => [ - 'visitor_id' => $customerId ? null : $visitorId, - 'customer_id' => $this->session->getCustomerId(), - 'added_at' => $productData['added_at'], - 'product_id' => $productId, - 'type_id' => $typeId - ] - ]); - - $this->entityManager->save($action); + $productIds = $this->getProductIdsByActions($productsData); + + if ($productIds) { + $collection->addFieldToFilter('product_id', $productIds); + + /** + * Note that collection is also filtered by visitor id and customer id + * This collection shouldnt be flushed when visitor has products and then login + * It can remove only products for visitor, or only products for customer + * + * ['product_id' => 'added_at'] + * @var ProductFrontendActionInterface $item + */ + foreach ($collection as $item) { + $this->entityManager->delete($item); + } + + foreach ($productsData as $productId => $productData) { + /** @var ProductFrontendActionInterface $action */ + $action = $this->productFrontendActionFactory->create([ + 'data' => [ + 'visitor_id' => $customerId ? null : $visitorId, + 'customer_id' => $this->session->getCustomerId(), + 'added_at' => $productData['added_at'], + 'product_id' => $productId, + 'type_id' => $typeId + ] + ]); + + $this->entityManager->save($action); + } } } diff --git a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php new file mode 100644 index 0000000000000..f6893a41113e6 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Type; + +use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Product\Price\SpecialPrice; +use Magento\Catalog\Api\Data\SpecialPriceInterface; +use Magento\Store\Api\Data\WebsiteInterface; + +/** + * Product special price model. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class FrontSpecialPrice extends Price +{ + /** + * @var SpecialPrice + */ + private $specialPrice; + + /** + * @param \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + * @param \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory + * @param \Magento\Framework\App\Config\ScopeConfigInterface $config + * @param SpecialPrice $specialPrice + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, + \Magento\Customer\Model\Session $customerSession, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + \Magento\Customer\Api\GroupManagementInterface $groupManagement, + \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory, + \Magento\Framework\App\Config\ScopeConfigInterface $config, + SpecialPrice $specialPrice + ) { + $this->specialPrice = $specialPrice; + parent::__construct( + $ruleFactory, + $storeManager, + $localeDate, + $customerSession, + $eventManager, + $priceCurrency, + $groupManagement, + $tierPriceFactory, + $config + ); + } + + /** + * @inheritdoc + */ + protected function _applySpecialPrice($product, $finalPrice) + { + if (!$product->getSpecialPrice()) { + return $finalPrice; + } + + $specialPrices = $this->getSpecialPrices($product); + $specialPrice = !(empty($specialPrices)) ? min($specialPrices) : $product->getSpecialPrice(); + + $specialPrice = $this->calculateSpecialPrice( + $finalPrice, + $specialPrice, + $product->getSpecialFromDate(), + $product->getSpecialToDate(), + WebsiteInterface::ADMIN_CODE + ); + $product->setData('special_price', $specialPrice); + + return $specialPrice; + } + + /** + * Get special prices. + * + * @param mixed $product + * @return array + */ + private function getSpecialPrices($product): array + { + $allSpecialPrices = $this->specialPrice->get([$product->getSku()]); + $specialPrices = []; + foreach ($allSpecialPrices as $price) { + if ($this->isSuitableSpecialPrice($product, $price)) { + $specialPrices[] = $price['value']; + } + } + + return $specialPrices; + } + + /** + * Price is suitable from default and current store + start and end date are equal. + * + * @param mixed $product + * @param array $price + * @return bool + */ + private function isSuitableSpecialPrice($product, array $price): bool + { + $priceStoreId = $price[Store::STORE_ID]; + if (($priceStoreId == Store::DEFAULT_STORE_ID || $product->getStoreId() == $priceStoreId) + && $price[SpecialPriceInterface::PRICE_FROM] == $product->getSpecialFromDate() + && $price[SpecialPriceInterface::PRICE_TO] == $product->getSpecialToDate()) { + return true; + } + + return false; + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php index 6614b10ef609e..1e55bd35553ab 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -9,10 +9,6 @@ namespace Magento\Catalog\Model\ResourceModel; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; -use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; -use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; -use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Catalog entity abstract model @@ -43,18 +39,16 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Factory $modelFactory * @param array $data - * @param UniqueValidationInterface|null $uniqueValidator */ public function __construct( \Magento\Eav\Model\Entity\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\Factory $modelFactory, - $data = [], - UniqueValidationInterface $uniqueValidator = null + $data = [] ) { $this->_storeManager = $storeManager; $this->_modelFactory = $modelFactory; - parent::__construct($context, $data, $uniqueValidator); + parent::__construct($context, $data); } /** @@ -94,7 +88,7 @@ protected function _isApplicableAttribute($object, $attribute) /** * Check whether attribute instance (attribute, backend, frontend or source) has method and applicable * - * @param AbstractAttribute|AbstractBackend|AbstractFrontend|AbstractSource $instance + * @param AbstractAttribute|\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend|\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend|\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource $instance * @param string $method * @param array $args array of arguments * @return boolean diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index e438e2f54113d..95f09c7ee80be 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -8,7 +8,6 @@ use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; -use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Product entity resource model @@ -102,7 +101,6 @@ class Product extends AbstractResource * @param \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes * @param array $data * @param TableMaintainer|null $tableMaintainer - * @param UniqueValidationInterface|null $uniqueValidator * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -117,8 +115,7 @@ public function __construct( \Magento\Eav\Model\Entity\TypeFactory $typeFactory, \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes, $data = [], - TableMaintainer $tableMaintainer = null, - UniqueValidationInterface $uniqueValidator = null + TableMaintainer $tableMaintainer = null ) { $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_catalogCategory = $catalogCategory; @@ -130,8 +127,7 @@ public function __construct( $context, $storeManager, $modelFactory, - $data, - $uniqueValidator + $data ); $this->connectionName = 'catalog'; $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); @@ -661,7 +657,7 @@ public function save(\Magento\Framework\Model\AbstractModel $object) } /** - * Retrieve entity manager object. + * Retrieve entity manager object * * @return \Magento\Framework\EntityManager\EntityManager */ @@ -675,7 +671,7 @@ private function getEntityManager() } /** - * Retrieve ProductWebsiteLink object. + * Retrieve ProductWebsiteLink object * * @deprecated 101.1.0 * @return ProductWebsiteLink @@ -686,7 +682,7 @@ private function getProductWebsiteLink() } /** - * Retrieve CategoryLink object. + * Retrieve CategoryLink object * * @deprecated 101.1.0 * @return \Magento\Catalog\Model\ResourceModel\Product\CategoryLink @@ -702,7 +698,6 @@ private function getProductCategoryLink() /** * Extends parent method to be appropriate for product. - * * Store id is required to correctly identify attribute value we are working with. * * @inheritdoc diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php index 179da06b59990..7e690ef3dbfc2 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Store\Model\ScopeInterface; /** * Catalog product custom option resource model @@ -154,21 +155,25 @@ protected function _saveValuePrices(\Magento\Framework\Model\AbstractModel $obje $scope = (int)$this->_config->getValue( \Magento\Store\Model\Store::XML_PATH_PRICE_SCOPE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); if ($object->getStoreId() != '0' && $scope == \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE) { - $baseCurrency = $this->_config->getValue( + $website = $this->_storeManager->getStore($object->getStoreId())->getWebsite(); + + $websiteBaseCurrency = $this->_config->getValue( \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, - 'default' + ScopeInterface::SCOPE_WEBSITE, + $website ); - $storeIds = $this->_storeManager->getStore($object->getStoreId())->getWebsite()->getStoreIds(); + $storeIds = $website->getStoreIds(); if (is_array($storeIds)) { foreach ($storeIds as $storeId) { if ($object->getPriceType() == 'fixed') { $storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); - $rate = $this->_currencyFactory->create()->load($baseCurrency)->getRate($storeCurrency); + $rate = $this->_currencyFactory->create()->load($websiteBaseCurrency) + ->getRate($storeCurrency); if (!$rate) { $rate = 1; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php index 5ffc9fbd575b6..3927b37016c1e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php @@ -17,11 +17,12 @@ use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Helper\Data; /** * Catalog product custom option resource model * - * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Value extends AbstractDb { @@ -51,6 +52,11 @@ class Value extends AbstractDb */ private $localeFormat; + /** + * @var Data + */ + private $dataHelper; + /** * Class constructor * @@ -59,17 +65,20 @@ class Value extends AbstractDb * @param StoreManagerInterface $storeManager * @param ScopeConfigInterface $config * @param string $connectionName + * @param Data $dataHelper */ public function __construct( Context $context, CurrencyFactory $currencyFactory, StoreManagerInterface $storeManager, ScopeConfigInterface $config, - $connectionName = null + $connectionName = null, + Data $dataHelper = null ) { $this->_currencyFactory = $currencyFactory; $this->_storeManager = $storeManager; $this->_config = $config; + $this->dataHelper = $dataHelper ?: ObjectManager::getInstance()->get(Data::class); parent::__construct($context, $connectionName); } @@ -130,7 +139,7 @@ protected function _saveValuePrices(AbstractModel $object) $optionTypeId = $this->getConnection()->fetchOne($select); if ($optionTypeId) { - if ($object->getStoreId() == '0') { + if ($object->getStoreId() == '0' || $this->dataHelper->isPriceGlobal()) { $bind = ['price' => $price, 'price_type' => $priceType]; $where = [ 'option_type_id = ?' => $optionTypeId, diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index d64dfb928e651..e68c75858102f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -86,6 +86,7 @@ <scrollToTopOfPage stepKey="initScrollToTopOfThePage"/> <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitSpecialPrice"/> + <conditionalClick selector="{{AdminProductFormAdvancedPricingSection.useDefaultPrice}}" dependentSelector="{{AdminProductFormAdvancedPricingSection.useDefaultPrice}}" visible="true" stepKey="checkUseDefault"/> <fillField userInput="{{price}}" selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="fillSpecialPrice"/> <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDone"/> <waitForElementNotVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitForCloseModalWindow"/> @@ -105,7 +106,7 @@ <actionGroup name="ProductSetAdvancedPricing"> <arguments> - <argument name="website"/> + <argument name="website" type="string" defaultValue="All Websites [USD]"/> <argument name="group" type="string" defaultValue="Retailer"/> <argument name="quantity" type="string" defaultValue="1"/> <argument name="price" type="string" defaultValue="Discount"/> @@ -116,7 +117,7 @@ <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAnd10percent"/> <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" stepKey="waitForSelectCustomerGroupNameAttribute"/> - <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{website.name}}" stepKey="selectProductWebsiteValue"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{website}}" stepKey="selectProductWebsiteValue"/> <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{group}}" stepKey="selectProductCustomGroupValue"/> <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{quantity}}" stepKey="fillProductTierPriceQtyInput"/> <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="{{price}}" stepKey="selectProductTierPriceValueType"/> @@ -257,4 +258,15 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveProductMessage"/> </actionGroup> + + <actionGroup name="AdminChangeProductAttributeSet"> + <arguments> + <argument name="attributeSet"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminProductFormSection.attributeSet}}" stepKey="startEditAttributeSet"/> + <fillField selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{attributeSet.attribute_set_name}}" stepKey="searchForAttributeSet"/> + <waitForElementVisible selector="{{AdminProductFormSection.attributeSetFilterResultByName(attributeSet.attribute_set_name)}}" stepKey="waitForNewAttributeSetIsShown"/> + <click selector="{{AdminProductFormSection.attributeSetFilterResultByName(attributeSet.attribute_set_name)}}" stepKey="selectAttributeSet"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml index a6213c459396a..f620b3c042a86 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml @@ -29,6 +29,17 @@ <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad" time="30"/> </actionGroup> + <!--Filter the product grid by the Name field--> + <actionGroup name="filterProductGridByName"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + </actionGroup> + <!--Delete a product by filtering grid and using delete action--> <actionGroup name="deleteProductUsingProductGrid"> <arguments> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml index c095faa73d9b1..e679d59fde791 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml @@ -6,22 +6,51 @@ */ --> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> - <!--Add a custom option of type "file" to a product--> - <actionGroup name="AddProductCustomOptionFile"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddProductCustomOption"> <arguments> - <argument name="option" defaultValue="ProductOptionFile"/> + <argument name="option"/> </arguments> <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" stepKey="waitForOption"/> <fillField selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{option.title}}" stepKey="fillTitle"/> <click selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}" stepKey="openTypeSelect"/> - <click selector="{{AdminProductCustomizableOptionsSection.optionType('File')}}" stepKey="selectTypeFile"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionType(option.type_label)}}" stepKey="selectTypeFile"/> <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" stepKey="waitForElements"/> <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" userInput="{{option.price}}" stepKey="fillPrice"/> <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensions}}" userInput="{{option.file_extension}}" stepKey="fillCompatibleExtensions"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku}}" userInput="{{option.title}}" stepKey="fillSku"/> + </actionGroup> + <!--Add a custom option of type "file" to a product--> + <actionGroup name="AddProductCustomOptionFile" extends="AddProductCustomOption"> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensions}}" userInput="{{option.file_extension}}" after="fillSku" stepKey="fillCompatibleExtensions"/> + </actionGroup> + <actionGroup name="ImportProductCustomizableOptions"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{AdminProductCustomizableOptionsSection.importOptions}}" stepKey="clickImportOptions"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsImportModalSection.selectProductTitle}}" stepKey="waitForTitleVisible"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickResetFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickFilterButton"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsImportModalSection.nameFilter}}" stepKey="waitForNameField"/> + <fillField selector="{{AdminProductCustomizableOptionsImportModalSection.nameFilter}}" userInput="{{productName}}" stepKey="fillProductName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFilters"/> + <checkOption selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="checkProductCheckbox"/> + <click selector="{{AdminProductCustomizableOptionsImportModalSection.importButton}}" stepKey="clickImport"/> + </actionGroup> + <actionGroup name="CheckCustomizableOptionImport"> + <arguments> + <argument name="option"/> + <argument name="optionIndex" type="string"/> + </arguments> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionTitleInputByIndex(optionIndex)}}" stepKey="grabOptionTitle"/> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionPriceByIndex(optionIndex)}}" stepKey="grabOptionPrice"/> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionSkuByIndex(optionIndex)}}" stepKey="grabOptionSku"/> + <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionTitle" stepKey="assertOptionTitle"/> + <assertEquals expected="{{option.price}}" expectedType="string" actual="$grabOptionPrice" stepKey="assertOptionPrice"/> + <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionSku" stepKey="assertOptionSku"/> </actionGroup> </actionGroups> + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml index 850190f0fd24c..d36877025e970 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml @@ -46,4 +46,26 @@ <entity name="DefaultFlatCatalogProduct" type="flat_catalog_product"> <data key="value">0</data> </entity> + + <entity name="UseFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> + <requiredEntity type="flat_catalog_product">UseFlatCatalogProduct</requiredEntity> + <requiredEntity type="flat_catalog_category">UseFlatCatalogCategory</requiredEntity> + </entity> + + <entity name="UseFlatCatalogProduct" type="flat_catalog_product"> + <data key="value">1</data> + </entity> + + <entity name="UseFlatCatalogCategory" type="flat_catalog_category"> + <data key="value">1</data> + </entity> + + <entity name="DefaultFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> + <requiredEntity type="flat_catalog_product">DefaultFlatCatalogProduct</requiredEntity> + <requiredEntity type="flat_catalog_category">DefaultFlatCatalogCategory</requiredEntity> + </entity> + + <entity name="DefaultFlatCatalogCategory" type="flat_catalog_category"> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index d5f29230f0308..b2483c3d2d080 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="productAttributeWithTwoOptions" type="ProductAttribute"> <data key="name" unique="suffix">ProductAttributeWithTwoOptions</data> <data key="attribute_code" unique="suffix">attribute</data> @@ -74,4 +74,7 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="ProductAttributeText" extends="productAttributeWithTwoOptions"> + <data key="frontend_input">text</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml index ace2f2e6a02ab..8ecae212b7c2d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml @@ -75,4 +75,12 @@ <requiredEntity type="StoreLabel">Option9Store0</requiredEntity> <requiredEntity type="StoreLabel">Option10Store1</requiredEntity> </entity> + <entity name="ProductAttributeAdminOption1" extends="productAttributeOption1"> + <requiredEntity type="StoreLabel">AdminOption1Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option1Store1</requiredEntity> + </entity> + <entity name="ProductAttributeAdminOption2" extends="productAttributeOption2"> + <requiredEntity type="StoreLabel">AdminOption2Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option2Store1</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 8ee8b49d0a10a..f6fb47c731790 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -277,6 +277,10 @@ <var key="sku" entityType="product" entityKey="sku" /> <requiredEntity type="product_option">ProductOptionDropDownWithLongValuesTitle</requiredEntity> </entity> + <entity name="ProductWithFileOption" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionFile</requiredEntity> + </entity> <entity name="SimpleProductWithCustomAttributeSet" type="product"> <data key="sku" unique="suffix">testSku</data> <data key="type_id">simple</data> @@ -302,4 +306,9 @@ <data key="quantity">100</data> <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> </entity> + <entity name="ProductWithFieldOptions" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionField</requiredEntity> + <requiredEntity type="product_option">ProductOptionField2</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml index ae8bcf0893ed0..82ce0d076f115 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml @@ -6,17 +6,24 @@ */ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="ProductOptionField" type="product_option"> <var key="product_sku" entityType="product" entityKey="sku" /> <data key="title">OptionField</data> + <data key="sku">OptionField</data> <data key="type">field</data> + <data key="type_label">Field</data> <data key="is_require">true</data> <data key="sort_order">1</data> <data key="price">10</data> <data key="price_type">fixed</data> <data key="max_characters">0</data> </entity> + <entity name="ProductOptionField2" type="product_option" extends="ProductOptionField"> + <data key="title">OptionField2</data> + <data key="sku">OptionField2</data> + <data key="price">20</data> + </entity> <entity name="ProductOptionArea" type="product_option"> <var key="product_sku" entityType="product" entityKey="sku" /> <data key="title">OptionArea</data> @@ -31,6 +38,7 @@ <var key="product_sku" entityType="product" entityKey="sku" /> <data key="title">OptionFile</data> <data key="type">file</data> + <data key="type_label">File</data> <data key="is_require">true</data> <data key="sort_order">3</data> <data key="price">9.99</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml index 37489ac8143b9..3af9b2c54a4f0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml @@ -72,4 +72,12 @@ <data key="store_id">1</data> <data key="label">Red</data> </entity> + <entity name="AdminOption1Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">admin_option_1</data> + </entity> + <entity name="AdminOption2Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">admin_option_2</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml index 8c8ae0ede2eb8..164ec8cb28a22 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml @@ -7,7 +7,7 @@ --> <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="AdminProductCreatePage" url="catalog/product/new/set/{{set}}/type/{{type}}/" area="admin" module="Magento_Catalog" parameterized="true"> <section name="AdminProductFormSection"/> <section name="AdminProductFormActionSection"/> @@ -18,5 +18,6 @@ <section name="AdminProductCustomizableOptionsSection" /> <section name="AdminAddProductsToOptionPanelSection" /> <section name="AdminProductFormAdvancedPricingSection"/> + <section name="AdminProductCustomizableOptionsImportModalSection"/> </page> </pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsImportModalSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsImportModalSection.xml new file mode 100644 index 0000000000000..0b1b5c966422a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsImportModalSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductCustomizableOptionsImportModalSection"> + <element name="selectProductTitle" type="text" selector="//aside[contains(@class, '_show')]//h1[normalize-space(text())='Select Product']"/> + <element name="nameFilter" type="input" selector="//aside[contains(@class, '_show')]//input[@name='name']"/> + <element name="importButton" type="button" selector="//aside[contains(@class, '_show')]//button[contains(@class, 'action-primary') and normalize-space(.)='Import']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml index 303fa5ec6b942..ca4e17fe92d6b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductCustomizableOptionsSection"> <element name="checkIfCustomizableOptionsTabOpen" type="text" selector="//span[text()='Customizable Options']/parent::strong/parent::*[@data-state-collapsible='closed']"/> <element name="customizableOptions" type="text" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Customizable Options']" timeout="30"/> @@ -26,8 +26,13 @@ <element name="lastOptionTypeParent" type="block" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__action-multiselect-text')]" /> <!-- var 1 represents the option type that you want to select, i.e "radio buttons" --> <element name="optionType" type="block" selector="//*[@data-index='custom_options']//label[text()='{{var1}}'][ancestor::*[contains(@class, '_active')]]" parameterized="true" /> - <element name="optionPrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price]']"/> - <element name="optionPriceType" type="select" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price_type]']"/> - <element name="optionFileExtensions" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][file_extension]']"/> + <element name="optionPrice" type="input" selector="[data-index='custom_options'] [data-index='options'] tbody tr:nth-last-of-type(1) input[name*='price']"/> + <element name="optionPriceType" type="select" selector="[data-index='custom_options'] [data-index='options'] tbody tr:nth-last-of-type(1) select[name*='price_type']"/> + <element name="optionFileExtensions" type="input" selector="[data-index='custom_options'] [data-index='options'] tbody tr:nth-last-of-type(1) input[name*='file_extension']"/> + <element name="optionSku" type="input" selector="[data-index='custom_options'] [data-index='options'] tbody tr:nth-last-of-type(1) input[name*='sku']"/> + <element name="optionTitleInputByIndex" type="input" selector="input[name='product[options][{{index}}][title]']" parameterized="true"/> + <element name="importOptions" type="button" selector="[data-index='custom_options'] [data-index='button_import']" timeout="30"/> + <element name="optionPriceByIndex" type="input" selector="input[name='product[options][{{index}}][price]']" parameterized="true"/> + <element name="optionSkuByIndex" type="input" selector="input[name='product[options][{{index}}][sku]']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml index ef66a41e27d06..9714c1f6eb483 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml @@ -21,5 +21,6 @@ <element name="productTierPricePercentageError" type="text" selector="div[data-index='percentage_value'] label.admin__field-error" /> <element name="specialPrice" type="input" selector="input[name='product[special_price]']"/> <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary" timeout="5"/> + <element name="useDefaultPrice" type="checkbox" selector="input[name='use_default[special_price]']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index 50b6fe6b3ec9e..2255f3fbbb3fb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -41,6 +41,7 @@ <element name="requiredNameIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('content');"/> <element name="requiredSkuIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=sku]>.admin__field-label span'), ':after').getPropertyValue('content');"/> <element name="customAttributeDropdownField" type="select" selector="select[name='product[{{attributeCode}}]']" parameterized="true"/> + <element name="customAttributeInputField" type="select" selector="input[name='product[{{attributeCode}}]']" parameterized="true"/> </section> <section name="ProductInWebsitesSection"> <element name="sectionHeader" type="button" selector="div[data-index='websites']" timeout="30"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml new file mode 100644 index 0000000000000..01967fa1e0851 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChangeProductAttributeSetTest"> + <annotations> + <features value="Catalog"/> + <stories value="Update product"/> + <title value="Attributes from the selected attribute set should be shown"/> + <description value="Attributes from the selected attribute set should be shown"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-15414"/> + <useCaseId value="MAGETWO-98380"/> + <group value="catalog"/> + </annotations> + <before> + <!--Create category product, attribute, attribute set--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ProductAttributeText" stepKey="createProductAttribute"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Assign attribute to attribute set--> + <amOnPage url="{{AdminProductAttributeSetEditPage.url($$createAttributeSet.attribute_set_id$$)}}" stepKey="openAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + </before> + <after> + <!--Delete created entities--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + + <actionGroup ref="logout" stepKey="logoutAdminUserAfterTest"/> + </after> + <!--Open created product--> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="openProductEditPage"/> + <waitForPageLoad time="30" stepKey="waitForProductPageIsLoaded"/> + <dontSeeElement selector="{{AdminProductFormSection.customAttributeInputField($$createProductAttribute.attribute_code$$)}}" stepKey="dontSeeCreatedAttribute"/> + <!--Change product attribute set--> + <actionGroup ref="AdminChangeProductAttributeSet" stepKey="changeProductAttributeSet"> + <argument name="attributeSet" value="$$createAttributeSet$$"/> + </actionGroup> + <!--Check new attribute is visible on product edit page--> + <seeElement selector="{{AdminProductFormSection.customAttributeInputField($$createProductAttribute.attribute_code$$)}}" stepKey="seeAttributeInForm"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckingAttributeValueOnProductEditPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckingAttributeValueOnProductEditPageTest.xml new file mode 100644 index 0000000000000..bd49f643148d9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckingAttributeValueOnProductEditPageTest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckingAttributeValueOnProductEditPageTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create/configure Dropdown product attribute"/> + <title value="Checking attribute values on a product edit page"/> + <description value="Checking attribute values on a product edit page"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15746"/> + <useCaseId value="MAGETWO-74037"/> + <group value="catalog"/> + </annotations> + <before> + <!--Create Dropdown product attribute--> + <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createDropdownProductAttribute"/> + <!--Add options to attribute--> + <createData entity="ProductAttributeAdminOption1" stepKey="createFirstOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <createData entity="ProductAttributeAdminOption2" stepKey="createSecondOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <!--Add attribute to Default Attribute Set--> + <createData entity="AddToDefaultSet" stepKey="attributeSet"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create Simple product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete product attribute--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createDropdownProductAttribute" stepKey="deleteDropdownProductAttribute"/> + <!--Logout--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Go to Product edit page--> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!--Click on attribute dropdown--> + <click selector="{{AdminProductFormSection.customAttributeDropdownField($$createDropdownProductAttribute.attribute[attribute_code]$$)}}" stepKey="clickOnAttributeDropdown"/> + <!--Check attribute dropdown options--> + <see selector="{{AdminProductFormSection.customAttributeDropdownField($$createDropdownProductAttribute.attribute[attribute_code]$$)}}" userInput="admin_option_1" stepKey="seeFirstAdminOption"/> + <see selector="{{AdminProductFormSection.customAttributeDropdownField($$createDropdownProductAttribute.attribute[attribute_code]$$)}}" userInput="admin_option_2" stepKey="seeSecondAdminOption"/> + <dontSee selector="{{AdminProductFormSection.customAttributeDropdownField($$createDropdownProductAttribute.attribute[attribute_code]$$)}}" userInput="option1" stepKey="dontSeeFirstStoreOption"/> + <dontSee selector="{{AdminProductFormSection.customAttributeDropdownField($$createDropdownProductAttribute.attribute[attribute_code]$$)}}" userInput="option2" stepKey="dontSeeSecondStoreOption"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml index 0fb4f2fd784e3..35b663da4f5ad 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml @@ -22,12 +22,10 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin2"/> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnPageAdminSystemStore"/> <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> - <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> - <waitForPageLoad time="10" stepKey="waitForPageAdminStoresGridLoadAfterResetButton"/> - <fillField selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="Main Website Store" stepKey="fillFieldOnWebsiteStore"/> - <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickOnSearchButton"/> - <waitForPageLoad stepKey="waitForPageAdminStoresGridLoadAfterSearchButton"/> - <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickOnstoreGrpNameInFirstRow"/> + <actionGroup ref="filterStoresGridByStore" stepKey="enterStoreGroup1Name"> + <argument name="store" value="Main Website Store"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.storeInFirstRow}}" stepKey="clickOnstoreInFirstRow"/> <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad1" /> <selectOption userInput="Default Category" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectOptionDefaultCategory"/> <click selector="{{AdminStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> @@ -59,12 +57,11 @@ <!--Assign new created root category to store--> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnPageAdminSystemStore"/> <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> - <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> - <waitForPageLoad time="10" stepKey="waitForPageAdminStoresGridLoadAfterResetButton"/> - <fillField selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="Main Website Store" stepKey="fillFieldOnWebsiteStore"/> - <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickOnSearchButton"/> + <actionGroup ref="filterStoresGridByStore" stepKey="enterStoreGroup1Name"> + <argument name="store" value="Main Website Store"/> + </actionGroup> <waitForPageLoad stepKey="waitForPageAdminStoresGridLoadAfterSearchButton"/> - <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickOnstoreGrpNameInFirstRow"/> + <click selector="{{AdminStoresGridSection.storeInFirstRow}}" stepKey="clickOnstoreInFirstRow"/> <waitForPageLoad stepKey="waitForPageAdminStoresGroupEditLoad" /> <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectOptionCreatedNewRootCategory"/> <click selector="{{AdminStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> @@ -80,4 +77,4 @@ <argument name="categoryEntity" value="SubCategoryWithParent"/> </actionGroup> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterProductGridByNameByStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterProductGridByNameByStoreViewTest.xml new file mode 100644 index 0000000000000..539d87b4a1064 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterProductGridByNameByStoreViewTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminFilterProductGridByNameByStoreViewTest"> + <annotations> + <features value="Catalog"/> + <stories value="Filter products"/> + <title value="Product grid filtering by store view level attribute"/> + <description value="Verify that products grid can be filtered on all store view level by attribute"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98755"/> + <useCaseId value="MAGETWO-97405"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleProduct3" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToEditPage"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToDefaultStoreView"> + <argument name="scopeName" value="_defaultStore.name"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillNewName"/> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="filterProductGridByName" stepKey="filterGridByName"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.firstRow}}" userInput="{{SimpleProduct3.name}}" stepKey="seeProductNameInGrid"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml new file mode 100644 index 0000000000000..ce9337dead3f7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminImportCustomizableOptionToProductWithSKUTest"> + <annotations> + <features value="Catalog"/> + <title value="Import customizable options to a product with existing SKU"/> + <description value="Import customizable options to a product with existing SKU"/> + <stories value="Import customizable options"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15740"/> + <useCaseId value="MAGETWO-73157"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct2" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <updateData createDataKey="createFirstProduct" entity="ProductWithFieldOptions" stepKey="updateProductCustomOptions" /> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="DeleteProductByName" stepKey="deleteSecondProduct"> + <argument name="product" value="$$createSecondProduct.name$$"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Change second product sku to first product sku--> + <amOnPage url="{{AdminProductEditPage.url($$createSecondProduct.id$$)}}" stepKey="goToSecondProductEditPage"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="$$createFirstProduct.sku$$" stepKey="fillProductSku"/> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + + <!--Import customizable options and check--> + <actionGroup ref="ImportProductCustomizableOptions" stepKey="importProductCustomOptions"> + <argument name="productName" value="$$createFirstProduct.name$$"/> + </actionGroup> + <actionGroup ref="CheckCustomizableOptionImport" stepKey="checkFirstOptionImport"> + <argument name="option" value="ProductOptionField"/> + <argument name="optionIndex" value="0"/> + </actionGroup> + <actionGroup ref="CheckCustomizableOptionImport" stepKey="checkSecondOptionImport"> + <argument name="option" value="ProductOptionField2"/> + <argument name="optionIndex" value="1"/> + </actionGroup> + + <!--Save product and check sku changed message--> + <actionGroup ref="saveProductForm" stepKey="saveSecondProduct"/> + <waitForElementVisible selector="{{AdminMessagesSection.noticeMessage}}" stepKey="waitForSkuChangedMessage"/> + <see selector="{{AdminMessagesSection.noticeMessage}}" userInput="SKU for product $$createSecondProduct.name$$ has been changed to $$createFirstProduct.sku$$-1." stepKey="seeSkuChangedMessage"/> + + <!-- Check that custom options are present on Admin product page in Customizable Option section after Product save --> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSectionAfterProductSave"/> + <actionGroup ref="CheckCustomizableOptionImport" stepKey="checkFirstCustomOptionAfterProductSave"> + <argument name="option" value="ProductOptionField"/> + <argument name="optionIndex" value="0"/> + </actionGroup> + <actionGroup ref="CheckCustomizableOptionImport" stepKey="checkSecondCustomOptionAfterProductSave"> + <argument name="option" value="ProductOptionField2"/> + <argument name="optionIndex" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index a1d9b4fb7b9a2..fd364682011c6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -71,7 +71,7 @@ <argument name="website" value="SecondWebsite"/> </actionGroup> <actionGroup ref="ProductSetAdvancedPricing" stepKey="setAdvancedPricingForProduct1"> - <argument name="website" value="SecondWebsite"/> + <argument name="website" value="{{SecondWebsite.name}}"/> </actionGroup> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct2"> @@ -84,7 +84,7 @@ <argument name="website" value="SecondWebsite"/> </actionGroup> <actionGroup ref="ProductSetAdvancedPricing" stepKey="setAdvancedPricingForProduct2"> - <argument name="website" value="SecondWebsite"/> + <argument name="website" value="{{SecondWebsite.name}}"/> </actionGroup> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct3"> @@ -97,7 +97,7 @@ <argument name="website" value="SecondWebsite"/> </actionGroup> <actionGroup ref="ProductSetAdvancedPricing" stepKey="setAdvancedPricingForProduct3"> - <argument name="website" value="SecondWebsite"/> + <argument name="website" value="{{SecondWebsite.name}}"/> </actionGroup> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct4"> @@ -110,7 +110,7 @@ <argument name="website" value="SecondWebsite"/> </actionGroup> <actionGroup ref="ProductSetAdvancedPricing" stepKey="setAdvancedPricingForProduct4"> - <argument name="website" value="SecondWebsite"/> + <argument name="website" value="{{SecondWebsite.name}}"/> </actionGroup> <!--Flush cache--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml index e4ea511efe46c..f333db0e65aa8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml @@ -58,19 +58,17 @@ <!-- Change root category for Main Website Store. --> <amOnPage stepKey="s1" url="{{AdminSystemStorePage.url}}"/> <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> - <click stepKey="s2" selector="{{AdminStoresGridSection.resetButton}}"/> - <waitForPageLoad stepKey="waitForPageAdminStoresGridLoadAfterResetButton" time="10"/> - <fillField stepKey="s4" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="Main Website Store"/> - <click stepKey="s5" selector="{{AdminStoresGridSection.searchButton}}"/> - <waitForPageLoad stepKey="waitForPageAdminStoresGridLoadAfterSearchButton"/> - <click stepKey="s7" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" /> + <actionGroup ref="filterStoresGridByStore" stepKey="filterStoresGridByStore"> + <argument name="store" value="Main Website Store"/> + </actionGroup> + <click stepKey="s7" selector="{{AdminStoresGridSection.storeInFirstRow}}" /> <waitForPageLoad stepKey="waitForPageAdminStoresGroupEditLoad" /> <selectOption selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="{{NewRootCategory.name}}" stepKey="setNewCategoryForStoreGroup"/> <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreGroup"/> <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModalSaveStoreGroup"/> <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptModal" /> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageAdminStoresGridReload"/> + <waitForElementVisible selector="{{AdminStoresGridFilterSection.filters}}" stepKey="waitForPageAdminStoresGridReload"/> <see userInput="You saved the store." stepKey="seeSavedMessage"/> <!-- @TODO: Uncomment commented below code after MQE-903 is fixed --> @@ -160,4 +158,4 @@ <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryDefaultCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaveDefaultCategory"/> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php new file mode 100644 index 0000000000000..102b810b0e0a8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php @@ -0,0 +1,223 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Test\Unit\Block\Product\View; + +use Magento\Catalog\Block\Product\Context; +use Magento\Catalog\Block\Product\View\Gallery; +use Magento\Catalog\Block\Product\View\GalleryOptions; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Escaper; +use Magento\Framework\View\Config; +use Magento\Framework\Config\View; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GalleryOptionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var GalleryOptions + */ + private $model; + + /** + * @var Gallery|\PHPUnit_Framework_MockObject_MockObject + */ + private $gallery; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var View|\PHPUnit_Framework_MockObject_MockObject + */ + private $configView; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $viewConfig; + + /** + * @var Escaper + */ + private $escaper; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->escaper = $objectManager->getObject(Escaper::class); + $this->configView = $this->createMock(View::class); + + $this->viewConfig = $this->createConfiguredMock( + Config::class, + [ + 'getViewConfig' => $this->configView + ] + ); + + $this->context = $this->createConfiguredMock( + Context::class, + [ + 'getEscaper' => $this->escaper, + 'getViewConfig' => $this->viewConfig + ] + ); + + $this->gallery = $this->createMock(Gallery::class); + + $this->jsonSerializer = $objectManager->getObject( + Json::class + ); + + $this->model = $objectManager->getObject(GalleryOptions::class, [ + 'context' => $this->context, + 'jsonSerializer' => $this->jsonSerializer, + 'gallery' => $this->gallery + ]); + } + + public function testGetOptionsJson() + { + $configMap = [ + ['Magento_Catalog', 'gallery/nav', 'thumbs'], + ['Magento_Catalog', 'gallery/loop', false], + ['Magento_Catalog', 'gallery/keyboard', true], + ['Magento_Catalog', 'gallery/arrows', true], + ['Magento_Catalog', 'gallery/caption', false], + ['Magento_Catalog', 'gallery/allowfullscreen', true], + ['Magento_Catalog', 'gallery/navdir', 'horizontal'], + ['Magento_Catalog', 'gallery/navarrows', true], + ['Magento_Catalog', 'gallery/navtype', 'slides'], + ['Magento_Catalog', 'gallery/thumbmargin', '5'], + ['Magento_Catalog', 'gallery/transition/effect', 'slide'], + ['Magento_Catalog', 'gallery/transition/duration', '500'], + ]; + + $imageAttributesMap = [ + ['product_page_image_medium','height',null, 100], + ['product_page_image_medium','width',null, 200], + ['product_page_image_small','height',null, 300], + ['product_page_image_small','width',null, 400] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + $this->gallery->expects($this->any()) + ->method('getImageAttribute') + ->will($this->returnValueMap($imageAttributesMap)); + + $json = $this->model->getOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertSame('thumbs', $decodedJson['nav']); + $this->assertSame(false, $decodedJson['loop']); + $this->assertSame(true, $decodedJson['keyboard']); + $this->assertSame(true, $decodedJson['arrows']); + $this->assertSame(false, $decodedJson['showCaption']); + $this->assertSame(true, $decodedJson['allowfullscreen']); + $this->assertSame('horizontal', $decodedJson['navdir']); + $this->assertSame(true, $decodedJson['navarrows']); + $this->assertSame('slides', $decodedJson['navtype']); + $this->assertSame(5, $decodedJson['thumbmargin']); + $this->assertSame('slide', $decodedJson['transition']); + $this->assertSame(500, $decodedJson['transitionduration']); + $this->assertSame(100, $decodedJson['height']); + $this->assertSame(200, $decodedJson['width']); + $this->assertSame(300, $decodedJson['thumbheight']); + $this->assertSame(400, $decodedJson['thumbwidth']); + } + + public function testGetFSOptionsJson() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/nav', false], + ['Magento_Catalog', 'gallery/fullscreen/loop', true], + ['Magento_Catalog', 'gallery/fullscreen/keyboard', true], + ['Magento_Catalog', 'gallery/fullscreen/arrows', false], + ['Magento_Catalog', 'gallery/fullscreen/caption', true], + ['Magento_Catalog', 'gallery/fullscreen/navdir', 'vertical'], + ['Magento_Catalog', 'gallery/fullscreen/navarrows', false], + ['Magento_Catalog', 'gallery/fullscreen/navtype', 'thumbs'], + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', '10'], + ['Magento_Catalog', 'gallery/fullscreen/transition/effect', 'dissolve'], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', '300'] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getFSOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + //Note, this tests the special case for nav variable set to false. It + //Should not be converted to boolean. + $this->assertSame('false', $decodedJson['nav']); + $this->assertSame(true, $decodedJson['loop']); + $this->assertSame(false, $decodedJson['arrows']); + $this->assertSame(true, $decodedJson['keyboard']); + $this->assertSame(true, $decodedJson['showCaption']); + $this->assertSame('vertical', $decodedJson['navdir']); + $this->assertSame(false, $decodedJson['navarrows']); + $this->assertSame(10, $decodedJson['thumbmargin']); + $this->assertSame('thumbs', $decodedJson['navtype']); + $this->assertSame('dissolve', $decodedJson['transition']); + $this->assertSame(300, $decodedJson['transitionduration']); + } + + public function testGetOptionsJsonOptionals() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertArrayNotHasKey('thumbmargin', $decodedJson); + $this->assertArrayNotHasKey('transitionduration', $decodedJson); + } + + public function testGetFSOptionsJsonOptionals() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/keyboard', false], + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getFSOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertArrayNotHasKey('thumbmargin', $decodedJson); + $this->assertArrayNotHasKey('keyboard', $decodedJson); + $this->assertArrayNotHasKey('transitionduration', $decodedJson); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php index 196b4df5b47c0..2cae2c07cc85a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/DeleteTest.php @@ -42,8 +42,9 @@ protected function setUp() false, true, true, - ['getParam', 'getPost'] + ['getParam', 'getPost', 'isPost'] ); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $auth = $this->createPartialMock(\Magento\Backend\Model\Auth::class, ['getAuthStorage']); $this->authStorage = $this->createPartialMock( \Magento\Backend\Model\Auth\StorageInterface::class, diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php index d729d0ffbdccc..3d5150fcc9f7e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/MoveTest.php @@ -83,9 +83,10 @@ private function fillContext() { $this->request = $this ->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->setMethods(['getPost']) + ->setMethods(['getPost', 'isPost']) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->context->expects($this->once())->method('getRequest')->will($this->returnValue($this->request)); $this->messageManager = $this->createMock(ManagerInterface::class); $this->context->expects($this->once())->method('getMessageManager')->willReturn($this->messageManager); diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php index de44af7f58afc..9dd0f6ef3ff71 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php @@ -229,6 +229,8 @@ protected function prepareContext() $this->objectManager->expects($this->any())->method('get')->will($this->returnValueMap([ [\Magento\CatalogInventory\Api\StockConfigurationInterface::class, $this->stockConfig], ])); + + $this->request->expects($this->any())->method('isPost')->willReturn(true); } public function testExecuteThatProductIdsAreObtainedFromAttributeHelper() diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php deleted file mode 100644 index a1aaab0995d73..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +++ /dev/null @@ -1,339 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; - -use Magento\Catalog\Api\Data\ProductAttributeInterface; -use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; -use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; -use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; -use Magento\Catalog\Model\Product\AttributeSet\Build; -use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; -use Magento\Eav\Api\Data\AttributeSetInterface; -use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; -use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\Serialize\Serializer\FormData; -use Magento\Framework\Controller\ResultFactory; -use Magento\Framework\Filter\FilterManager; -use Magento\Catalog\Helper\Product as ProductHelper; -use Magento\Framework\View\Element\Messages; -use Magento\Framework\View\LayoutFactory; -use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; -use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator as InputTypeValidator; -use Magento\Framework\View\LayoutInterface; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SaveTest extends AttributeTest -{ - /** - * @var BuildFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $buildFactoryMock; - - /** - * @var FilterManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterManagerMock; - - /** - * @var ProductHelper|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productHelperMock; - - /** - * @var AttributeFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $attributeFactoryMock; - - /** - * @var ValidatorFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $validatorFactoryMock; - - /** - * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $groupCollectionFactoryMock; - - /** - * @var LayoutFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $layoutFactoryMock; - - /** - * @var ResultRedirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $redirectMock; - - /** - * @var AttributeSet|\PHPUnit_Framework_MockObject_MockObject - */ - protected $attributeSetMock; - - /** - * @var Build|\PHPUnit_Framework_MockObject_MockObject - */ - protected $builderMock; - - /** - * @var InputTypeValidator|\PHPUnit_Framework_MockObject_MockObject - */ - protected $inputTypeValidatorMock; - - /** - * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $messageManagerMock; - - /** - * @var FormData|\PHPUnit_Framework_MockObject_MockObject - */ - private $formDataSerializerMock; - - /** - * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $productAttributeMock; - - protected function setUp() - { - parent::setUp(); - $this->buildFactoryMock = $this->getMockBuilder(BuildFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->filterManagerMock = $this->getMockBuilder(FilterManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productHelperMock = $this->getMockBuilder(ProductHelper::class) - ->disableOriginalConstructor() - ->getMock(); - $this->attributeFactoryMock = $this->getMockBuilder(AttributeFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->validatorFactoryMock = $this->getMockBuilder(ValidatorFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->groupCollectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->layoutFactoryMock = $this->getMockBuilder(LayoutFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) - ->setMethods(['setData', 'setPath']) - ->disableOriginalConstructor() - ->getMock(); - $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->builderMock = $this->getMockBuilder(Build::class) - ->disableOriginalConstructor() - ->getMock(); - $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) - ->setMethods(['getId', 'get']) - ->getMockForAbstractClass(); - - $this->buildFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->builderMock); - $this->validatorFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->inputTypeValidatorMock); - $this->attributeFactoryMock - ->method('create') - ->willReturn($this->productAttributeMock); - } - - /** - * {@inheritdoc} - */ - protected function getModel() - { - return $this->objectManager->getObject(Save::class, [ - 'context' => $this->contextMock, - 'messageManager' => $this->messageManagerMock, - 'attributeLabelCache' => $this->attributeLabelCacheMock, - 'coreRegistry' => $this->coreRegistryMock, - 'resultPageFactory' => $this->resultPageFactoryMock, - 'buildFactory' => $this->buildFactoryMock, - 'filterManager' => $this->filterManagerMock, - 'productHelper' => $this->productHelperMock, - 'attributeFactory' => $this->attributeFactoryMock, - 'validatorFactory' => $this->validatorFactoryMock, - 'groupCollectionFactory' => $this->groupCollectionFactoryMock, - 'layoutFactory' => $this->layoutFactoryMock, - 'formDataSerializer' => $this->formDataSerializerMock, - ]); - } - - public function testExecuteWithEmptyData() - { - $this->requestMock->expects($this->any()) - ->method('getParam') - ->willReturnMap([ - ['isAjax', null, null], - ['serialized_options', '[]', ''], - ]); - $this->formDataSerializerMock->expects($this->once()) - ->method('unserialize') - ->with('') - ->willReturn([]); - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn([]); - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->redirectMock); - $this->redirectMock->expects($this->any()) - ->method('setPath') - ->willReturnSelf(); - - $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); - } - - public function testExecute() - { - $data = [ - 'new_attribute_set_name' => 'Test attribute set name', - 'frontend_input' => 'test_frontend_input', - ]; - - $this->requestMock->expects($this->any()) - ->method('getParam') - ->willReturnMap([ - ['isAjax', null, null], - ['serialized_options', '[]', ''], - ]); - $this->formDataSerializerMock->expects($this->once()) - ->method('unserialize') - ->with('') - ->willReturn([]); - $this->productAttributeMock->expects($this->once()) - ->method('getId') - ->willReturn(1); - $this->productAttributeMock->expects($this->once()) - ->method('getAttributeCode') - ->willReturn('test_code'); - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($data); - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->redirectMock); - $this->redirectMock->expects($this->any()) - ->method('setPath') - ->willReturnSelf(); - $this->builderMock->expects($this->once()) - ->method('setEntityTypeId') - ->willReturnSelf(); - $this->builderMock->expects($this->once()) - ->method('setSkeletonId') - ->willReturnSelf(); - $this->builderMock->expects($this->once()) - ->method('setName') - ->willReturnSelf(); - $this->builderMock->expects($this->once()) - ->method('getAttributeSet') - ->willReturn($this->attributeSetMock); - $this->requestMock->expects($this->any()) - ->method('getParam') - ->willReturnMap([ - ['set', null, 1], - ['attribute_code', null, 'test_attribute_code'] - ]); - $this->inputTypeValidatorMock->expects($this->once()) - ->method('getMessages') - ->willReturn([]); - - $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); - } - - /** - * @return void - * @throws \Magento\Framework\Exception\NotFoundException - */ - public function testExecuteWithOptionsDataError() - { - $serializedOptions = '{"key":"value"}'; - $message = "The attribute couldn't be saved due to an error. Verify your information and try again. " - . "If the error persists, please try again later."; - - $this->requestMock->expects($this->any()) - ->method('getParam') - ->willReturnMap([ - ['isAjax', null, true], - ['serialized_options', '[]', $serializedOptions], - ]); - $this->formDataSerializerMock->expects($this->once()) - ->method('unserialize') - ->with($serializedOptions) - ->willThrowException(new \InvalidArgumentException('Some exception')); - $this->messageManagerMock->expects($this->once()) - ->method('addErrorMessage') - ->with($message); - $this->addReturnResultConditions('catalog/*/edit', ['_current' => true], ['error' => true]); - - $this->getModel()->execute(); - } - - /** - * @param string $path - * @param array $params - * @param array $response - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - private function addReturnResultConditions(string $path = '', array $params = [], array $response = []) - { - $layoutMock = $this->getMockBuilder(LayoutInterface::class) - ->setMethods(['initMessages', 'getMessagesBlock']) - ->getMockForAbstractClass(); - $this->layoutFactoryMock->expects($this->once()) - ->method('create') - ->with() - ->willReturn($layoutMock); - $layoutMock->expects($this->once()) - ->method('initMessages') - ->with(); - $messageBlockMock = $this->getMockBuilder(Messages::class) - ->disableOriginalConstructor() - ->getMock(); - $layoutMock->expects($this->once()) - ->method('getMessagesBlock') - ->willReturn($messageBlockMock); - $messageBlockMock->expects($this->once()) - ->method('getGroupedHtml') - ->willReturn('message1'); - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(ResultFactory::TYPE_JSON) - ->willReturn($this->redirectMock); - $response = array_merge($response, [ - 'messages' => ['message1'], - 'params' => $params, - ]); - $this->redirectMock->expects($this->once()) - ->method('setData') - ->with($response) - ->willReturnSelf(); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php index d41de5f67503c..e2b9b06175b13 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/MassStatusTest.php @@ -50,6 +50,9 @@ class MassStatusTest extends \Magento\Catalog\Test\Unit\Controller\Adminhtml\Pro */ private $actionMock; + /** + * @inheritdoc + */ protected function setUp() { $this->priceProcessorMock = $this->getMockBuilder(Processor::class) @@ -111,6 +114,7 @@ protected function setUp() ]; /** @var \Magento\Backend\App\Action\Context $context */ $context = $this->initContext($additionalParams, [[Action::class, $this->actionMock]]); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->action = new \Magento\Catalog\Controller\Adminhtml\Product\MassStatus( $context, diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/SaveTest.php index a10814371577e..22dbc32c2c3ab 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/SaveTest.php @@ -5,10 +5,15 @@ */ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product; +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; +use Magento\Catalog\Model\Product\Copier; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; /** + * Unit tests for \Magento\Catalog\Controller\Adminhtml\Product\Save class. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SaveTest extends \Magento\Catalog\Test\Unit\Controller\Adminhtml\ProductTest @@ -28,6 +33,21 @@ class SaveTest extends \Magento\Catalog\Test\Unit\Controller\Adminhtml\ProductTe /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ private $product; + /** + * @var Copier|\PHPUnit_Framework_MockObject_MockObject + */ + private $productCopierMock; + + /** + * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productAttributeMock; + + /** + * @var CategoryLinkManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryLinkManagementMock; + /** @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ private $resultRedirectFactory; @@ -42,6 +62,7 @@ class SaveTest extends \Magento\Catalog\Test\Unit\Controller\Adminhtml\ProductTe /** * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { @@ -49,11 +70,33 @@ protected function setUp() \Magento\Catalog\Controller\Adminhtml\Product\Builder::class, ['build'] ); - $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)->disableOriginalConstructor() - ->setMethods(['addData', 'getSku', 'getTypeId', 'getStoreId', '__sleep', '__wakeup'])->getMock(); + $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addData', + 'unsetData', + 'getData', + 'getSku', + 'getCategoryIds', + 'getAttributes', + 'getTypeId', + 'getStoreId', + 'save', + '__sleep', + '__wakeup', + ]) + ->getMock(); $this->product->expects($this->any())->method('getTypeId')->will($this->returnValue('simple')); $this->product->expects($this->any())->method('getStoreId')->will($this->returnValue('1')); $this->productBuilder->expects($this->any())->method('build')->will($this->returnValue($this->product)); + $this->productCopierMock = $this->getMockBuilder(Copier::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) + ->setMethods(['getIsUnique', 'getIsUserDefined', 'getAttributeCode', 'getDefaultFrontendLabel']) + ->getMockForAbstractClass(); + $this->categoryLinkManagementMock = $this->getMockBuilder(CategoryLinkManagementInterface::class) + ->getMockForAbstractClass(); $this->messageManagerMock = $this->getMockForAbstractClass( \Magento\Framework\Message\ManagerInterface::class @@ -155,4 +198,86 @@ public function exceptionTypeDataProvider() ['Exception', 'addErrorMessage'] ]; } + + /** + * @return void + */ + public function testExecuteCheckUniqueAttributesOnDuplicate() + { + $productSku = 'test_sku'; + $attributeCode = 'test_attribute_code'; + + $productData = [ + 'product' => [ + 'name' => 'test-name', + 'sku' => $productSku, + $attributeCode => 'test_attribute', + ] + ]; + + $this->request->expects($this->at(1)) + ->method('getParam') + ->with('back', false) + ->willReturn('duplicate'); + + $this->request->expects($this->any())->method('getPostValue')->willReturn($productData); + $this->initializationHelper->expects($this->any())->method('initialize') + ->willReturn($this->product); + + $this->product->expects($this->once()) + ->method('save') + ->willReturnSelf(); + $this->product->expects($this->any()) + ->method('getSku') + ->willReturn($productSku); + $this->product->expects($this->any()) + ->method('getCategoryIds') + ->willReturn([]); + + $this->categoryLinkManagementMock->expects($this->any()) + ->method('assignProductToCategories') + ->with($productSku, []) + ->willReturn(true); + + $this->product->expects($this->once()) + ->method('unsetData') + ->with('quantity_and_stock_status') + ->willReturnSelf(); + + $this->productCopierMock->expects($this->any()) + ->method('copy') + ->with($this->product) + ->willReturn($this->product); + + $this->product->expects($this->once()) + ->method('getAttributes') + ->willReturn([$this->productAttributeMock]); + + $this->productAttributeMock->expects($this->atLeastOnce()) + ->method('getIsUnique') + ->willReturn('1'); + $this->productAttributeMock->expects($this->atLeastOnce()) + ->method('getIsUserDefined') + ->willReturn('1'); + $this->productAttributeMock->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + + $this->product->expects($this->any()) + ->method('getData') + ->willReturnMap([ + [$attributeCode, null, $productData['product'][$attributeCode]] + ]); + + $this->productAttributeMock->expects($this->atLeastOnce()) + ->method('getDefaultFrontendLabel') + ->willReturn('Test Attribute Label'); + + $this->messageManagerMock->expects($this->once()) + ->method('addErrorMessage'); + $this->messageManagerMock->expects($this->atLeastOnce()) + ->method('addSuccessMessage'); + + $this->action->execute(); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/ProductTest.php index 5d08f80847da2..7c3b78d5cf05a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/ProductTest.php @@ -67,7 +67,7 @@ protected function initContext(array $additionalParams = [], array $objectManage ->setMethods(['add', 'prepend'])->disableOriginalConstructor()->getMock(); $title->expects($this->any())->method('prepend')->withAnyParameters()->will($this->returnSelf()); $requestInterfaceMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class)->setMethods( - ['getParam', 'getPost', 'getFullActionName', 'getPostValue'] + ['getParam', 'getPost', 'getFullActionName', 'getPostValue', 'isPost'] )->disableOriginalConstructor()->getMock(); $responseInterfaceMock = $this->getMockBuilder(\Magento\Framework\App\ResponseInterface::class)->setMethods( diff --git a/app/code/Magento/Catalog/Test/Unit/Cron/FrontendActionsFlushTest.php b/app/code/Magento/Catalog/Test/Unit/Cron/FrontendActionsFlushTest.php index 29d9736e02442..46eac31af986e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Cron/FrontendActionsFlushTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Cron/FrontendActionsFlushTest.php @@ -87,13 +87,14 @@ public function testExecute() 'recently_viewed_product' ]); + $time = time() - 1500; $connectionMock->expects($this->once()) ->method('quoteInto') - ->with('added_at < ?', time() - 1500) - ->willReturn(['added_at < ?', time() - 1500]); + ->with('added_at < ?', $time) + ->willReturn(['added_at < ?', $time]); $connectionMock->expects($this->once()) ->method('delete') - ->with('catalog_product_frontend_action', [['added_at < ?', time() - 1500]]); + ->with('catalog_product_frontend_action', [['added_at < ?', $time]]); $this->model->execute(); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php index ab0c0ea38d79c..b8b76524099f4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php @@ -86,16 +86,14 @@ public function testGetList() $categoryIdSecond = 2; $categoryFirst = $this->getMockBuilder(Category::class)->disableOriginalConstructor()->getMock(); - $categoryFirst->expects($this->atLeastOnce())->method('getId')->willReturn($categoryIdFirst); $categorySecond = $this->getMockBuilder(Category::class)->disableOriginalConstructor()->getMock(); - $categorySecond->expects($this->atLeastOnce())->method('getId')->willReturn($categoryIdSecond); /** @var SearchCriteriaInterface|\PHPUnit_Framework_MockObject_MockObject $searchCriteria */ $searchCriteria = $this->createMock(SearchCriteriaInterface::class); $collection = $this->getMockBuilder(Collection::class)->disableOriginalConstructor()->getMock(); $collection->expects($this->once())->method('getSize')->willReturn($totalCount); - $collection->expects($this->once())->method('getItems')->willReturn([$categoryFirst, $categorySecond]); + $collection->expects($this->once())->method('getAllIds')->willReturn([$categoryIdFirst, $categoryIdSecond]); $this->collectionProcessorMock->expects($this->once()) ->method('process') diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php index 5b1d3bf7943fc..0688ad5bde19d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php @@ -36,6 +36,14 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ private $attribute; + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) @@ -62,54 +70,78 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->escaperMock = $this->getMockBuilder( + \Magento\Framework\Escaper::class + ) + ->disableOriginalConstructor() + ->setMethods(['escapeHtml']) + ->getMock(); + $helper = new ObjectManager($this); $this->model = $helper->getObject( \Magento\Catalog\Model\Config\CatalogClone\Media\Image::class, [ 'eavConfig' => $this->eavConfig, - 'attributeCollectionFactory' => $this->attributeCollectionFactory + 'attributeCollectionFactory' => $this->attributeCollectionFactory, + 'escaper' => $this->escaperMock, ] ); } - public function testGetPrefixes() + /** + * @param string $actualLabel + * @param string $expectedLabel + * @return void + * @dataProvider getPrefixesDataProvider + */ + public function testGetPrefixes(string $actualLabel, string $expectedLabel) { $entityTypeId = 3; /** @var \Magento\Eav\Model\Entity\Type|\PHPUnit_Framework_MockObject_MockObject $entityType */ $entityType = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) ->disableOriginalConstructor() ->getMock(); - $entityType->expects($this->once())->method('getId')->will($this->returnValue($entityTypeId)); + $entityType->expects($this->once())->method('getId')->willReturn($entityTypeId); /** @var AbstractFrontend|\PHPUnit_Framework_MockObject_MockObject $frontend */ $frontend = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend::class) ->setMethods(['getLabel']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $frontend->expects($this->once())->method('getLabel')->will($this->returnValue('testLabel')); + $frontend->expects($this->once())->method('getLabel')->willReturn($actualLabel); - $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with( - $this->equalTo($entityTypeId) - ); - $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with( - $this->equalTo('media_image') - ); + $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with($entityTypeId); + $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with('media_image'); - $this->attribute->expects($this->once())->method('getAttributeCode')->will( - $this->returnValue('attributeCode') - ); - $this->attribute->expects($this->once())->method('getFrontend')->will( - $this->returnValue($frontend) - ); + $this->attribute->expects($this->once())->method('getAttributeCode')->willReturn('attributeCode'); + $this->attribute->expects($this->once())->method('getFrontend')->willReturn($frontend); - $this->attributeCollection->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$this->attribute])) - ); + $this->attributeCollection->expects($this->any())->method('getIterator') + ->willReturn(new \ArrayIterator([$this->attribute])); + + $this->eavConfig->expects($this->any())->method('getEntityType')->with(Product::ENTITY) + ->willReturn($entityType); + + $this->escaperMock->expects($this->once())->method('escapeHtml')->with($actualLabel) + ->willReturn($expectedLabel); - $this->eavConfig->expects($this->any())->method('getEntityType')->with( - $this->equalTo(Product::ENTITY) - )->will($this->returnValue($entityType)); + $this->assertEquals([['field' => 'attributeCode_', 'label' => $expectedLabel]], $this->model->getPrefixes()); + } - $this->assertEquals([['field' => 'attributeCode_', 'label' => 'testLabel']], $this->model->getPrefixes()); + /** + * @return array + */ + public function getPrefixesDataProvider(): array + { + return [ + [ + 'actual_label' => 'testLabel', + 'expected_label' => 'testLabel', + ], + [ + 'actual_label' => '<media-image-attributelabel', + 'expected_label' => '<media-image-attributelabel', + ], + ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php index cf9e83ed39650..967a2167c688a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php @@ -5,13 +5,23 @@ */ namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Eav\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Product\Eav\Action\Full; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\BatchProviderInterface; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; +use PHPUnit\Framework\MockObject\MockObject as MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -19,60 +29,69 @@ class FullTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full|\PHPUnit_Framework_MockObject_MockObject + * @var Full|MockObject */ private $model; /** - * @var DecimalFactory|\PHPUnit_Framework_MockObject_MockObject + * @var DecimalFactory|MockObject */ private $eavDecimalFactory; /** - * @var SourceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var SourceFactory|MockObject */ private $eavSourceFactory; /** - * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject + * @var MetadataPool|MockObject */ private $metadataPool; /** - * @var BatchProviderInterface|\PHPUnit_Framework_MockObject_MockObject + * @var BatchProviderInterface|MockObject */ private $batchProvider; /** - * @var BatchSizeCalculator|\PHPUnit_Framework_MockObject_MockObject + * @var BatchSizeCalculator|MockObject */ private $batchSizeCalculator; /** - * @var ActiveTableSwitcher|\PHPUnit_Framework_MockObject_MockObject + * @var ActiveTableSwitcher|MockObject */ private $activeTableSwitcher; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ScopeConfigInterface|MockObject */ private $scopeConfig; + /** + * @var Generator + */ + private $batchQueryGenerator; + + /** + * @inheritdoc + */ protected function setUp() { $this->eavDecimalFactory = $this->createPartialMock(DecimalFactory::class, ['create']); $this->eavSourceFactory = $this->createPartialMock(SourceFactory::class, ['create']); $this->metadataPool = $this->createMock(MetadataPool::class); $this->batchProvider = $this->getMockForAbstractClass(BatchProviderInterface::class); + $this->batchQueryGenerator = $this->createMock(Generator::class); $this->batchSizeCalculator = $this->createMock(BatchSizeCalculator::class); $this->activeTableSwitcher = $this->createMock(ActiveTableSwitcher::class); - $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $objectManager = new ObjectManager($this); $this->model = $objectManager->getObject( - \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full::class, + Full::class, [ 'eavDecimalFactory' => $this->eavDecimalFactory, 'eavSourceFactory' => $this->eavSourceFactory, @@ -80,7 +99,8 @@ protected function setUp() 'batchProvider' => $this->batchProvider, 'batchSizeCalculator' => $this->batchSizeCalculator, 'activeTableSwitcher' => $this->activeTableSwitcher, - 'scopeConfig' => $this->scopeConfig + 'scopeConfig' => $this->scopeConfig, + 'batchQueryGenerator' => $this->batchQueryGenerator, ] ); } @@ -93,15 +113,15 @@ public function testExecute() $this->scopeConfig->expects($this->once())->method('getValue')->willReturn(1); $ids = [1, 2, 3]; - $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + $connectionMock = $this->getMockBuilder(AdapterInterface::class) ->getMockForAbstractClass(); $connectionMock->expects($this->atLeastOnce())->method('describeTable')->willReturn(['id' => []]); - $eavSource = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source::class) + $eavSource = $this->getMockBuilder(Source::class) ->disableOriginalConstructor() ->getMock(); - $eavDecimal = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal::class) + $eavDecimal = $this->getMockBuilder(Decimal::class) ->disableOriginalConstructor() ->getMock(); @@ -122,22 +142,28 @@ public function testExecute() $this->eavSourceFactory->expects($this->once())->method('create')->will($this->returnValue($eavDecimal)); - $entityMetadataMock = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + $entityMetadataMock = $this->getMockBuilder(EntityMetadataInterface::class) ->getMockForAbstractClass(); $this->metadataPool->expects($this->atLeastOnce()) ->method('getMetadata') - ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->with(ProductInterface::class) ->willReturn($entityMetadataMock); - $this->batchProvider->expects($this->atLeastOnce()) - ->method('getBatches') - ->willReturn([['from' => 10, 'to' => 100]]); - $this->batchProvider->expects($this->atLeastOnce()) - ->method('getBatchIds') + // Super inefficient algorithm in some cases + $this->batchProvider->expects($this->never()) + ->method('getBatches'); + + $batchQuery = $this->createMock(Select::class); + + $connectionMock->method('fetchCol') + ->with($batchQuery) ->willReturn($ids); - $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + $this->batchQueryGenerator->method('generate') + ->willReturn([$batchQuery]); + + $selectMock = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() ->getMock(); @@ -148,6 +174,9 @@ public function testExecute() $this->model->execute(); } + /** + * @return void + */ public function testExecuteWithDisabledEavIndexer() { $this->scopeConfig->expects($this->once())->method('getValue')->willReturn(0); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php index 6fe0594be08f2..7b830124a365b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php @@ -48,7 +48,12 @@ public function setUp() $this->model = new SaveHandler($this->optionRepository); } - public function testExecute() + /** + * @dataProvider testExecuteDataProvider + * @param bool $dataHasChangedFor + * @return void + */ + public function testExecute(bool $dataHasChangedFor) { $this->optionMock->expects($this->any())->method('getOptionId')->willReturn(5); $this->entity->expects($this->once())->method('getOptions')->willReturn([$this->optionMock]); @@ -63,10 +68,27 @@ public function testExecute() ->method('getProductOptions') ->with($this->entity) ->willReturn([$this->optionMock, $secondOptionMock]); - + $this->entity->expects($this->once()) + ->method('dataHasChangedFor') + ->with('sku') + ->willReturn($dataHasChangedFor); + $this->entity->expects($this->once()) + ->method('getSku') + ->willReturn('product_sku'); $this->optionRepository->expects($this->once())->method('delete'); $this->optionRepository->expects($this->once())->method('save')->with($this->optionMock); $this->assertEquals($this->entity, $this->model->execute($this->entity)); } + + /** + * @return array + */ + public function testExecuteDataProvider(): array + { + return [ + [true], + [false], + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php index fce4a02622d9e..2fd787e216118 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php @@ -78,27 +78,20 @@ protected function setUp() ); } - public function testFilterProductActions() + /** + * @dataProvider filterProductActionsDataProvider + * + * @param array $productsData + * @param bool $correct + * @return void + */ + public function testFilterProductActions(array $productsData, bool $correct) { - $productsData = [ - 1 => [ - 'added_at' => 12, - 'product_id' => 1, - ], - 2 => [ - 'added_at' => 13, - 'product_id' => 2, - ], - 3 => [ - 'added_at' => 14, - 'product_id' => 3, - ] - ]; $frontendConfiguration = $this->createMock(\Magento\Catalog\Model\FrontendStorageConfigurationInterface::class); $frontendConfiguration->expects($this->once()) ->method('get') ->willReturn([ - 'lifetime' => 2 + 'lifetime' => 2, ]); $this->frontendStorageConfigurationPoolMock->expects($this->once()) ->method('get') @@ -110,7 +103,6 @@ public function testFilterProductActions() $action2 = $this->getMockBuilder(ProductFrontendActionInterface::class) ->getMockForAbstractClass(); - $frontendAction = $this->createMock(ProductFrontendActionInterface::class); $collection = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); @@ -126,47 +118,91 @@ public function testFilterProductActions() $collection->expects($this->once()) ->method('addFilterByUserIdentities') ->with(1, 34); - $collection->expects($this->any()) - ->method('addFieldToFilter') - ->withConsecutive(['type_id'], ['product_id']); - $iterator = new \IteratorIterator(new \ArrayIterator([$frontendAction])); - $collection->expects($this->once()) - ->method('getIterator') - ->willReturn($iterator); - $this->entityManagerMock->expects($this->once()) - ->method('delete') - ->with($frontendAction); - $this->productFrontendActionFactoryMock->expects($this->exactly(2)) - ->method('create') - ->withConsecutive( - [ + if ($correct) { + $frontendAction = $this->createMock(ProductFrontendActionInterface::class); + $iterator = new \IteratorIterator(new \ArrayIterator([$frontendAction])); + $collection->expects($this->any()) + ->method('addFieldToFilter') + ->withConsecutive(['type_id'], ['product_id']); + $collection->expects($this->once()) + ->method('getIterator') + ->willReturn($iterator); + $this->entityManagerMock->expects($this->once()) + ->method('delete') + ->with($frontendAction); + $this->entityManagerMock->expects($this->exactly(2)) + ->method('save') + ->withConsecutive([$action1], [$action2]); + $this->productFrontendActionFactoryMock->expects($this->exactly(2)) + ->method('create') + ->withConsecutive( [ - 'data' => [ - 'visitor_id' => null, - 'customer_id' => 1, - 'added_at' => 12, - 'product_id' => 1, - 'type_id' => 'recently_compared_product' - ] - ] - ], - [ + [ + 'data' => [ + 'visitor_id' => null, + 'customer_id' => 1, + 'added_at' => 12, + 'product_id' => 1, + 'type_id' => 'recently_compared_product', + ], + ], + ], [ - 'data' => [ - 'visitor_id' => null, - 'customer_id' => 1, - 'added_at' => 13, - 'product_id' => 2, - 'type_id' => 'recently_compared_product' - ] + [ + 'data' => [ + 'visitor_id' => null, + 'customer_id' => 1, + 'added_at' => 13, + 'product_id' => 2, + 'type_id' => 'recently_compared_product', + ], + ], ] - ] - ) - ->willReturnOnConsecutiveCalls($action1, $action2); - $this->entityManagerMock->expects($this->exactly(2)) - ->method('save') - ->withConsecutive([$action1], [$action2]); + ) + ->willReturnOnConsecutiveCalls($action1, $action2); + } else { + $this->entityManagerMock->expects($this->never()) + ->method('delete'); + $this->entityManagerMock->expects($this->never()) + ->method('save'); + } + $this->model->syncActions($productsData, 'recently_compared_product'); } + + /** + * @return array + */ + public function filterProductActionsDataProvider(): array + { + return [ + [ + 'productsData' => [ + 1 => [ + 'added_at' => 12, + 'product_id' => 1, + ], + 2 => [ + 'added_at' => 13, + 'product_id' => 2, + ], + 3 => [ + 'added_at' => 14, + 'product_id' => 3, + ], + ], + 'correct' => true, + ], + [ + 'productsData' => [ + 1 => [ + 'added_at' => 12, + 'product_id' => 'test', + ], + ], + 'correct' => false, + ], + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php new file mode 100644 index 0000000000000..c4a7aa4037bec --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Ui\Component; + +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Ui\Component\ColumnFactory; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\ColumnInterface; +use Magento\Ui\Component\Filters\FilterModifier; + +/** + * ColumnFactory test. + */ +class ColumnFactoryTest extends TestCase +{ + /** + * @var ColumnFactory + */ + private $columnFactory; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attribute; + + /** + * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + /** + * @var ColumnInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $column; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->attribute = $this->getMockBuilder(ProductAttributeInterface::class) + ->setMethods(['usesSource']) + ->getMockForAbstractClass(); + $this->context = $this->createMock(ContextInterface::class); + $this->uiComponentFactory = $this->createMock(UiComponentFactory::class); + $this->column = $this->getMockForAbstractClass(ColumnInterface::class); + $this->uiComponentFactory->method('create') + ->willReturn($this->column); + + $this->columnFactory = $this->objectManager->getObject(ColumnFactory::class, [ + 'componentFactory' => $this->uiComponentFactory + ]); + } + + /** + * Tests the create method will return correct object. + * + * @return void + */ + public function testCreatedObject() + { + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn([]); + + $object = $this->columnFactory->create($this->attribute, $this->context); + $this->assertEquals( + $this->column, + $object, + 'Object must be the same which the ui component factory creates.' + ); + } + + /** + * Tests create method with not filterable in grid attribute. + * + * @param array $filterModifiers + * @param null|string $filter + * + * @return void + * @dataProvider filterModifiersProvider + */ + public function testCreateWithNotFilterableInGridAttribute(array $filterModifiers, $filter) + { + $componentFactoryArgument = [ + 'data' => [ + 'config' => [ + 'label' => __(null), + 'dataType' => 'text', + 'add_field' => true, + 'visible' => null, + 'filter' => $filter, + 'component' => 'Magento_Ui/js/grid/columns/column', + ], + ], + 'context' => $this->context, + ]; + + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn($filterModifiers); + $this->attribute->method('getIsFilterableInGrid') + ->willReturn(false); + $this->attribute->method('getAttributeCode') + ->willReturn('color'); + + $this->uiComponentFactory->expects($this->once()) + ->method('create') + ->with($this->anything(), $this->anything(), $componentFactoryArgument); + + $this->columnFactory->create($this->attribute, $this->context); + } + + /** + * Filter modifiers data provider. + * + * @return array + */ + public function filterModifiersProvider(): array + { + return [ + 'without' => [ + 'filter_modifiers' => [], + 'filter' => null, + ], + 'with' => [ + 'filter_modifiers' => [ + 'color' => [ + 'condition_type' => 'notnull', + ], + ], + 'filter' => 'text', + ], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index cc3dda6e2d7b1..e741070547163 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -155,38 +155,4 @@ public function modifyMetaLockedDataProvider() { return [[true], [false]]; } - - public function testModifyMetaWithCaching() - { - $this->arrayManagerMock->expects($this->exactly(2)) - ->method('findPath') - ->willReturn(true); - $cacheManager = $this->getMockBuilder(CacheInterface::class) - ->getMockForAbstractClass(); - $cacheManager->expects($this->once()) - ->method('load') - ->with(Categories::CATEGORY_TREE_ID . '_'); - $cacheManager->expects($this->once()) - ->method('save'); - - $modifier = $this->createModel(); - $cacheContextProperty = new \ReflectionProperty( - Categories::class, - 'cacheManager' - ); - $cacheContextProperty->setAccessible(true); - $cacheContextProperty->setValue($modifier, $cacheManager); - - $groupCode = 'test_group_code'; - $meta = [ - $groupCode => [ - 'children' => [ - 'category_ids' => [ - 'sortOrder' => 10, - ], - ], - ], - ]; - $modifier->modifyMeta($meta); - } } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php index 6d7c8814bd474..35daac491d583 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php @@ -54,7 +54,16 @@ protected function setUp() ->getMockForAbstractClass(); $this->collectionMock = $this->getMockBuilder(AbstractCollection::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getSelect', 'getTable', 'getIterator', 'isLoaded', 'toArray', 'getSize']) + ->setMethods([ + 'load', + 'getSelect', + 'getTable', + 'getIterator', + 'isLoaded', + 'toArray', + 'getSize', + 'setStoreId', + ]) ->getMockForAbstractClass(); $this->dbSelectMock = $this->getMockBuilder(DbSelect::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php index cbc67fee8a5a3..40687e37e1538 100644 --- a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Ui\Component; +use Magento\Ui\Component\Filters\FilterModifier; + /** * @api * @since 100.0.2 @@ -54,13 +56,15 @@ public function __construct(\Magento\Framework\View\Element\UiComponentFactory $ */ public function create($attribute, $context, array $config = []) { + $filterModifiers = $context->getRequestParam(FilterModifier::FILTER_MODIFIER, []); + $columnName = $attribute->getAttributeCode(); $config = array_merge([ 'label' => __($attribute->getDefaultFrontendLabel()), 'dataType' => $this->getDataType($attribute), 'add_field' => true, 'visible' => $attribute->getIsVisibleInGrid(), - 'filter' => ($attribute->getIsFilterableInGrid()) + 'filter' => ($attribute->getIsFilterableInGrid() || array_key_exists($columnName, $filterModifiers)) ? $this->getFilterType($attribute->getFrontendInput()) : null, ], $config); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 2dad7e8495b11..a3baf7b14a229 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Model\Locator\LocatorInterface; @@ -11,6 +13,7 @@ use Magento\Framework\App\CacheInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; @@ -202,6 +205,7 @@ protected function createNewCategoryModal(array $meta) * * @param array $meta * @return array + * @throws LocalizedException * @since 101.0.0 */ protected function customizeCategoriesField(array $meta) @@ -306,20 +310,64 @@ protected function customizeCategoriesField(array $meta) * * @param string|null $filter * @return array + * @throws LocalizedException * @since 101.0.0 */ protected function getCategoriesTree($filter = null) { - $categoryTree = $this->getCacheManager()->load(self::CATEGORY_TREE_ID . '_' . $filter); - if ($categoryTree) { - return $this->serializer->unserialize($categoryTree); + $storeId = (int) $this->locator->getStore()->getId(); + + $cachedCategoriesTree = $this->getCacheManager() + ->load($this->getCategoriesTreeCacheId($storeId, (string) $filter)); + if (!empty($cachedCategoriesTree)) { + return $this->serializer->unserialize($cachedCategoriesTree); } - $storeId = $this->locator->getStore()->getId(); + $categoriesTree = $this->retrieveCategoriesTree( + $storeId, + $this->retrieveShownCategoriesIds($storeId, (string) $filter) + ); + + $this->getCacheManager()->save( + $this->serializer->serialize($categoriesTree), + $this->getCategoriesTreeCacheId($storeId, (string) $filter), + [ + \Magento\Catalog\Model\Category::CACHE_TAG, + \Magento\Framework\App\Cache\Type\Block::CACHE_TAG + ] + ); + + return $categoriesTree; + } + + /** + * Get cache id for categories tree. + * + * @param int $storeId + * @param string $filter + * @return string + */ + private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string + { + return self::CATEGORY_TREE_ID + . '_' . (string) $storeId + . '_' . $filter; + } + + /** + * Retrieve filtered list of categories id. + * + * @param int $storeId + * @param string $filter + * @return array + * @throws LocalizedException + */ + private function retrieveShownCategoriesIds(int $storeId, string $filter = '') : array + { /* @var $matchingNamesCollection \Magento\Catalog\Model\ResourceModel\Category\Collection */ $matchingNamesCollection = $this->categoryCollectionFactory->create(); - if ($filter !== null) { + if (!empty($filter)) { $matchingNamesCollection->addAttributeToFilter( 'name', ['like' => $this->dbHelper->addLikeEscape($filter, ['position' => 'any'])] @@ -339,6 +387,19 @@ protected function getCategoriesTree($filter = null) } } + return $shownCategoriesIds; + } + + /** + * Retrieve tree of categories with attributes. + * + * @param int $storeId + * @param array $shownCategoriesIds + * @return array|null + * @throws LocalizedException + */ + private function retrieveCategoriesTree(int $storeId, array $shownCategoriesIds) + { /* @var $collection \Magento\Catalog\Model\ResourceModel\Category\Collection */ $collection = $this->categoryCollectionFactory->create(); @@ -365,15 +426,6 @@ protected function getCategoriesTree($filter = null) $categoryById[$category->getParentId()]['optgroup'][] = &$categoryById[$category->getId()]; } - $this->getCacheManager()->save( - $this->serializer->serialize($categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']), - self::CATEGORY_TREE_ID . '_' . $filter, - [ - \Magento\Catalog\Model\Category::CACHE_TAG, - \Magento\Framework\App\Cache\Type\Block::CACHE_TAG - ] - ); - return $categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']; } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 6a6d74c6cb9b3..af898bf3117c6 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -655,7 +655,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done $attributeModel = $this->getAttributeModel($attribute); if ($attributeModel->usesSource()) { - $options = $attributeModel->getSource()->getAllOptions(); + $options = $attributeModel->getSource()->getAllOptions(true, true); $meta = $this->arrayManager->merge($configPath, $meta, [ 'options' => $this->convertOptionsValueToString($options), ]); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php index 1a9b9f205d701..9111bf544d52a 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php @@ -115,7 +115,7 @@ private function getUpdatedTierPriceStructure(array $priceMeta) 'dataType' => Price::NAME, 'component' => 'Magento_Ui/js/form/components/group', 'label' => __('Price'), - 'enableLabel' => true, + 'showLabel' => false, 'dataScope' => '', 'additionalClasses' => 'control-grouped', 'sortOrder' => isset($priceMeta['arguments']['data']['config']['sortOrder']) diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php index f4334bc25efd8..29a19036f3bf3 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php @@ -5,6 +5,10 @@ */ namespace Magento\Catalog\Ui\DataProvider\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\Exception\LocalizedException; +use Magento\Eav\Model\Entity\Attribute\AttributeInterface; + /** * Collection which is used for rendering product list in the backend. * @@ -25,4 +29,63 @@ protected function _productLimitationJoinPrice() $this->_productLimitationFilters->setUsePriceIndex(false); return $this->_productLimitationPrice(true); } + + /** + * Add attribute filter to collection + * + * @param AttributeInterface|integer|string|array $attribute + * @param null|string|array $condition + * @param string $joinType + * @return $this + * @throws LocalizedException + */ + public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner') + { + $storeId = (int)$this->getStoreId(); + if ($attribute === 'is_saleable' + || is_array($attribute) + || $storeId !== $this->getDefaultStoreId() + ) { + return parent::addAttributeToFilter($attribute, $condition, $joinType); + } + + if ($attribute instanceof AttributeInterface) { + $attributeModel = $attribute; + } else { + $attributeModel = $this->getEntity()->getAttribute($attribute); + if ($attributeModel === false) { + throw new LocalizedException( + __('Invalid attribute identifier for filter (%1)', get_class($attribute)) + ); + } + } + + if ($attributeModel->isScopeGlobal() || $attributeModel->getBackend()->isStatic()) { + return parent::addAttributeToFilter($attribute, $condition, $joinType); + } + + $this->addAttributeToFilterAllStores($attributeModel, $condition); + + return $this; + } + + /** + * Add attribute to filter by all stores + * + * @param Attribute $attributeModel + * @param array $condition + * @return void + */ + private function addAttributeToFilterAllStores(Attribute $attributeModel, array $condition) + { + $tableName = $this->getTable($attributeModel->getBackendTable()); + $entity = $this->getEntity(); + $fKey = 'e.' . $this->getEntityPkName($entity); + $pKey = $tableName . '.' . $this->getEntityPkName($entity); + $condition = "({$pKey} = {$fKey}) AND (" + . $this->_getConditionSql("{$tableName}.value", $condition) + . ')'; + $selectExistsInAllStores = $this->getConnection()->select()->from($tableName); + $this->getSelect()->exists($selectExistsInAllStores, $condition); + } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php index e31eca89f63c2..44aa33367c6be 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Ui\DataProvider\Product; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Store\Model\Store; /** * Class ProductDataProvider @@ -58,6 +59,7 @@ public function __construct( $this->collection = $collectionFactory->create(); $this->addFieldStrategies = $addFieldStrategies; $this->addFilterStrategies = $addFilterStrategies; + $this->collection->setStoreId(Store::DEFAULT_STORE_ID); } /** diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 127b14e3b9d85..893f8d7190700 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -34,7 +34,7 @@ "magento/module-catalog-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "102.0.7", + "version": "102.0.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index 9c99a72c12d1c..6a432c1809ba5 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -56,7 +56,7 @@ <field id="grid_per_page_values" translate="label comment" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="grid_per_page" translate="label comment" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Default Value</label> @@ -66,7 +66,7 @@ <field id="list_per_page_values" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="list_per_page" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Default Value</label> diff --git a/app/code/Magento/Catalog/etc/frontend/di.xml b/app/code/Magento/Catalog/etc/frontend/di.xml index 95391b656380f..4203af383b366 100644 --- a/app/code/Magento/Catalog/etc/frontend/di.xml +++ b/app/code/Magento/Catalog/etc/frontend/di.xml @@ -98,4 +98,5 @@ <type name="Magento\Framework\App\Action\AbstractAction"> <plugin name="catalog_app_action_dispatch_controller_context_plugin" type="Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin" /> </type> + <preference for="Magento\Catalog\Model\Product\Type\Price" type="Magento\Catalog\Model\Product\Type\FrontSpecialPrice" /> </config> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml index 30c05c2ec689b..9f83d42392e03 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml @@ -12,8 +12,10 @@ <?php $_optionId = $_option->getId(); ?> <div class="admin__field field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> <label class="label admin__field-label"> - <?= $block->escapeHtml($_option->getTitle()) ?> - <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> + <span> + <?= $block->escapeHtml($_option->getTitle()) ?> + <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> + </span> </label> <div class="admin__field-control control"> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml index 4ad7a95c91980..dd40cc68ac1ed 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml @@ -64,8 +64,10 @@ require(['prototype'], function(){ <div class="admin__field <?php if ($_option->getIsRequire()) echo ' required _required' ?>"> <label class="admin__field-label label"> - <?= $block->escapeHtml($_option->getTitle()) ?> - <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> + <span> + <?= $block->escapeHtml($_option->getTitle()) ?> + <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> + </span> </label> <div class="admin__field-control control"> <?php if ($_fileExists): ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml index 11fba22ea8139..1580cec60b3fa 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml @@ -11,8 +11,10 @@ <?php $_option = $block->getOption(); ?> <div class="field admin__field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> <label class="admin__field-label label"> - <?= $block->escapeHtml($_option->getTitle()) ?> - <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> + <span> + <?= $block->escapeHtml($_option->getTitle()) ?> + <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> + </span> </label> <div class="control admin__field-control"> <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD): ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml index efc06d675c369..64c8ba7dcf49f 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml @@ -30,6 +30,13 @@ }); </script> +<?php +$defaultMinSaleQty = $block->getDefaultConfigValue('min_sale_qty'); +if (!is_numeric($defaultMinSaleQty)) { + $defaultMinSaleQty = json_decode($defaultMinSaleQty, true); + $defaultMinSaleQty = (float) $defaultMinSaleQty[\Magento\Customer\Api\Data\GroupInterface::CUST_GROUP_ALL] ?? 1; +} +?> <div class="fieldset-wrapper form-inline advanced-inventory-edit"> <div class="fieldset-wrapper-title"> <strong class="title"> @@ -132,7 +139,7 @@ <div class="field"> <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[min_sale_qty]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('min_sale_qty') * 1 ?>" + value="<?= /* @escapeNotVerified */ $defaultMinSaleQty ?>" disabled="disabled"/> </div> <div class="field choice"> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js index 407fd1fe28e39..e1923dc46d68e 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js @@ -5,13 +5,13 @@ define([ 'jquery', - 'mage/mage' + 'mage/mage', + 'validation' ], function ($) { 'use strict'; return function (config, element) { - - $(element).mage('form').mage('validation', { + $(element).mage('form').validation({ validationUrl: config.validationUrl }); }; diff --git a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml new file mode 100644 index 0000000000000..0e2635f27c4b9 --- /dev/null +++ b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +use Magento\Catalog\Model\Product\Option; + +/** + * @var \Magento\Catalog\Block\Product\View\Options\Type\Select\Checkable $block + */ +$option = $block->getOption(); +if ($option) : ?> + <?php + $configValue = $block->getPreconfiguredValue($option); + $optionType = $option->getType(); + $arraySign = $optionType === Option::OPTION_TYPE_CHECKBOX ? '[]' : ''; + $count = 1; + ?> + + <div class="options-list nested" id="options-<?php echo /* @noEscape */ + $option->getId() ?>-list"> + <?php if ($optionType === Option::OPTION_TYPE_RADIO && !$option->getIsRequire()): ?> + <div class="field choice admin__field admin__field-option"> + <input type="radio" + id="options_<?php echo /* @noEscape */ + $option->getId() ?>" + class="radio admin__control-radio product-custom-option" + name="options[<?php echo /* @noEscape */ + $option->getId() ?>]" + data-selector="options[<?php echo /* @noEscape */ + $option->getId() ?>]" + onclick="<?php echo $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" + value="" + checked="checked" + /> + <label class="label admin__field-label" for="options_<?php echo /* @noEscape */ + $option->getId() ?>"> + <span> + <?php echo /* @noEscape */ + __('None') ?> + </span> + </label> + </div> + <?php endif; ?> + + <?php foreach ($option->getValues() as $value) : ?> + <?php + $checked = ''; + $count++; + if ($arraySign) { + $checked = is_array($configValue) && in_array($value->getOptionTypeId(), $configValue) ? 'checked' : ''; + } else { + $checked = $configValue == $value->getOptionTypeId() ? 'checked' : ''; + } + $dataSelector = 'options[' . $option->getId() . ']'; + if ($arraySign) { + $dataSelector .= '[' . $value->getOptionTypeId() . ']'; + } + ?> + + <div class="field choice admin__field admin__field-option <?php echo /* @noEscape */ + $option->getIsRequire() ? 'required': '' ?>"> + <input type="<?php echo /* @noEscape */ + $optionType ?>" + class="<?php /** @noinspection DisconnectedForeachInstructionInspection */ + echo /* @noEscape */ + $optionType === Option::OPTION_TYPE_RADIO ? + 'radio admin__control-radio' : + 'checkbox admin__control-checkbox' ?> <?php echo /* @noEscape */ + $option->getIsRequire() ? 'required': '' ?> + product-custom-option + <?php echo $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" + name="options[<?php echo $option->getId() ?>]<?php echo /* @noEscape */ + $arraySign ?>" + id="options_<?php echo /* @noEscape */ + $option->getId() . '_' . $count ?>" + value="<?php echo /* @noEscape */ + $value->getOptionTypeId() ?>" + <?php echo /* @noEscape */ + $checked ?> + data-selector="<?php echo /* @noEscape */ + $dataSelector ?>" + price="<?php echo /* @noEscape */ + $block->getCurrencyByStore($value) ?>" + /> + <label class="label admin__field-label" + for="options_<?php echo /* @noEscape */ + $option->getId() . '_' . $count ?>"> + <span> + <?php echo $block->escapeHtml($value->getTitle()) ?> + </span> + <?php echo /* @noEscape */ + $block->formatPrice($value) ?> + </label> + </div> + <?php endforeach; ?> + </div> +<?php endif; ?> \ No newline at end of file diff --git a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml index 3630fddb326a7..aa6ffa1bd33c4 100644 --- a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml @@ -121,7 +121,11 @@ </arguments> </block> </container> - <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"/> + <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"> + <arguments> + <argument name="gallery_options" xsi:type="object">Magento\Catalog\Block\Product\View\GalleryOptions</argument> + </arguments> + </block> <container name="skip_gallery_after.wrapper" htmlTag="div" htmlClass="action-skip-wrapper"> <block class="Magento\Framework\View\Element\Template" after="product.info.media.image" name="skip_gallery_after" template="Magento_Theme::html/skip.phtml"> <arguments> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml index c930d2195a01b..1c4a37fedebe3 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml @@ -23,8 +23,8 @@ <tbody> <?php foreach ($_additional as $_data): ?> <tr> - <th class="col label" scope="row"><?= $block->escapeHtml(__($_data['label'])) ?></th> - <td class="col data" data-th="<?= $block->escapeHtml(__($_data['label'])) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> + <th class="col label" scope="row"><?= $block->escapeHtml($_data['label']) ?></th> + <td class="col data" data-th="<?= $block->escapeHtml($_data['label']) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> </tr> <?php endforeach; ?> </tbody> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml index b2fa8e9aaf80f..0f65e39284392 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml @@ -43,79 +43,8 @@ "mixins":["magnifier/magnify"], "magnifierOpts": <?= /* @noEscape */ $block->getMagnifier() ?>, "data": <?= /* @noEscape */ $block->getGalleryImagesJson() ?>, - "options": { - "nav": "<?= $block->escapeHtml($block->getVar("gallery/nav")) ?>", - <?php if (($block->getVar("gallery/loop"))) : ?> - "loop": <?= $block->escapeHtml($block->getVar("gallery/loop")) ?>, - <?php endif; ?> - <?php if (($block->getVar("gallery/keyboard"))) : ?> - "keyboard": <?= $block->escapeHtml($block->getVar("gallery/keyboard")) ?>, - <?php endif; ?> - <?php if (($block->getVar("gallery/arrows"))) : ?> - "arrows": <?= $block->escapeHtml($block->getVar("gallery/arrows")) ?>, - <?php endif; ?> - <?php if (($block->getVar("gallery/allowfullscreen"))) : ?> - "allowfullscreen": <?= $block->escapeHtml($block->getVar("gallery/allowfullscreen")) ?>, - <?php endif; ?> - <?php if (is_bool($block->getVar("gallery/caption"))) : ?> - "showCaption": <?= /* @noEscape */ $block->getVar("gallery/caption") ? 'true' : 'false'; ?>, - <?php endif; ?> - <?php - $imgWidth = $block->getImageAttribute('product_page_image_medium', 'width'); - $thumbWidth = $block->getImageAttribute('product_page_image_small', 'width'); - ?> - "width": "<?= $block->escapeHtml($imgWidth) ?>", - "thumbwidth": "<?= $block->escapeHtml($thumbWidth) ?>", - <?php - $thumbHeight = $block->getImageAttribute('product_page_image_small', 'height') - ?: $block->getImageAttribute('product_page_image_small', 'width'); - ?> - <?php if ($thumbHeight) : ?> - "thumbheight": <?= $block->escapeHtml($thumbHeight); ?>, - <?php endif; ?> - <?php if (($block->getVar("gallery/thumbmargin"))) : ?> - "thumbmargin": <?= (int)$block->getVar("gallery/thumbmargin"); ?>, - <?php endif; ?> - <?php - $imgHeight = $block->getImageAttribute('product_page_image_medium', 'height') - ?: $block->getImageAttribute('product_page_image_medium', 'width') - ?> - <?php if ($imgHeight) : ?> - "height": <?= $block->escapeHtml($imgHeight); ?>, - <?php endif; ?> - <?php if ($block->getVar("gallery/transition/duration")) : ?> - "transitionduration": <?= $block->escapeHtml($block->getVar("gallery/transition/duration")) ?>, - <?php endif; ?> - "transition": "<?= $block->escapeHtml($block->getVar("gallery/transition/effect")) ?>", - <?php if (($block->getVar("gallery/navarrows"))) : ?> - "navarrows": <?= $block->escapeHtml($block->getVar("gallery/navarrows")) ?>, - <?php endif; ?> - "navtype": "<?= $block->escapeHtml($block->getVar("gallery/navtype")) ?>", - "navdir": "<?= $block->escapeHtml($block->getVar("gallery/navdir")) ?>" - }, - "fullscreen": { - "nav": "<?= $block->escapeHtml($block->getVar("gallery/fullscreen/nav")) ?>", - <?php if ($block->getVar("gallery/fullscreen/loop")) : ?> - "loop": <?= $block->escapeHtml($block->getVar("gallery/fullscreen/loop")) ?>, - <?php endif; ?> - "navdir": "<?= $block->escapeHtml($block->getVar("gallery/fullscreen/navdir")) ?>", - <?php if ($block->getVar("gallery/transition/navarrows")) : ?> - "navarrows": <?= $block->escapeHtml($block->getVar("gallery/fullscreen/navarrows")) ?>, - <?php endif; ?> - "navtype": "<?= $block->escapeHtml($block->getVar("gallery/fullscreen/navtype")) ?>", - <?php if ($block->getVar("gallery/fullscreen/arrows")) : ?> - "arrows": <?= $block->escapeHtml($block->getVar("gallery/fullscreen/arrows")) ?>, - <?php endif; ?> - <?php if (is_bool($block->getVar("gallery/fullscreen/caption"))) : ?> - <?php $showCaption = $block->getVar("gallery/fullscreen/caption") ? 'true' : 'false'; ?> - "showCaption": <?= /* @noEscape */ $showCaption ?>, - <?php endif; ?> - <?php if ($block->getVar("gallery/fullscreen/transition/duration")) : ?> - "transitionduration": <?= - $block->escapeHtml($block->getVar("gallery/fullscreen/transition/duration")) ?>, - <?php endif; ?> - "transition": "<?= $block->escapeHtml($block->getVar("gallery/fullscreen/transition/effect")) ?>" - }, + "options": <?= /* @noEscape */ $block->getGalleryOptions()->getOptionsJson() ?>, + "fullscreen": <?= /* @noEscape */ $block->getGalleryOptions()->getFSOptionsJson() ?>, "breakpoints": <?= /* @noEscape */ $block->getBreakpoints() ?> } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 9cb6d4bd2390d..d37b755e8c57a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -2838,7 +2838,8 @@ protected function getProductUrlSuffix($storeId = null) protected function getUrlKey($rowData) { if (!empty($rowData[self::URL_KEY])) { - return $this->productUrl->formatUrlKey($rowData[self::URL_KEY]); + $urlKey = (string) $rowData[self::URL_KEY]; + return trim(strtolower($urlKey)); } if (!empty($rowData[self::COL_NAME])) { diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json index 60115e63403cf..180fdb95a4d9b 100644 --- a/app/code/Magento/CatalogImportExport/composer.json +++ b/app/code/Magento/CatalogImportExport/composer.json @@ -16,7 +16,7 @@ "ext-ctype": "*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php index bc10d38173b4d..f5c26d4294927 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php @@ -6,12 +6,19 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogInventory\Model\Indexer\Stock\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement; +use Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock; use Magento\Framework\App\ResourceConnection; use Magento\CatalogInventory\Model\ResourceModel\Indexer\StockFactory; use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Framework\DB\Query\BatchIteratorInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\Indexer\CacheContext; use Magento\Framework\Event\ManagerInterface as EventManager; use Magento\Framework\EntityManager\MetadataPool; @@ -25,7 +32,6 @@ /** * Class Full reindex action * - * @package Magento\CatalogInventory\Model\Indexer\Stock\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Full extends AbstractAction @@ -60,6 +66,11 @@ class Full extends AbstractAction */ private $activeTableSwitcher; + /** + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + /** * @param ResourceConnection $resource * @param StockFactory $indexerFactory @@ -71,7 +82,7 @@ class Full extends AbstractAction * @param BatchProviderInterface|null $batchProvider * @param array $batchRowsCount * @param ActiveTableSwitcher|null $activeTableSwitcher - * + * @param QueryGenerator|null $batchQueryGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -84,7 +95,8 @@ public function __construct( BatchSizeManagementInterface $batchSizeManagement = null, BatchProviderInterface $batchProvider = null, array $batchRowsCount = [], - ActiveTableSwitcher $activeTableSwitcher = null + ActiveTableSwitcher $activeTableSwitcher = null, + QueryGenerator $batchQueryGenerator = null ) { parent::__construct( $resource, @@ -97,11 +109,12 @@ public function __construct( $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get(BatchProviderInterface::class); $this->batchSizeManagement = $batchSizeManagement ?: ObjectManager::getInstance()->get( - \Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement::class + BatchSizeManagement::class ); $this->batchRowsCount = $batchRowsCount; $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance() ->get(ActiveTableSwitcher::class); + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get(QueryGenerator::class); } /** @@ -109,9 +122,7 @@ public function __construct( * * @param null|array $ids * @throws LocalizedException - * * @return void - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($ids = null) @@ -120,11 +131,11 @@ public function execute($ids = null) $this->useIdxTable(false); $this->cleanIndexersTables($this->_getTypeIndexers()); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); $columns = array_keys($this->_getConnection()->describeTable($this->_getIdxTable())); - /** @var \Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\DefaultStock $indexer */ + /** @var DefaultStock $indexer */ foreach ($this->_getTypeIndexers() as $indexer) { $indexer->setActionType(self::ACTION_TYPE); $connection = $indexer->getConnection(); @@ -135,22 +146,21 @@ public function execute($ids = null) : $this->batchRowsCount['default']; $this->batchSizeManagement->ensureBatchSize($connection, $batchRowCount); - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), + + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), - $batchRowCount + $select, + $batchRowCount, + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR ); - foreach ($batches as $batch) { + foreach ($batchQueries as $query) { $this->clearTemporaryIndexTable(); - // Get entity ids from batch - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $select->where('type_id = ?', $indexer->getTypeId()); - - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); + $entityIds = $connection->fetchCol($query); if (!empty($entityIds)) { $indexer->reindexEntity($entityIds); $select = $connection->select()->from($this->_getIdxTable(), $columns); @@ -167,6 +177,7 @@ public function execute($ids = null) /** * Delete all records from index table + * * Used to clean table before re-indexation * * @param array $indexers diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/ProductStockOptionsData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/ProductStockOptionsData.xml deleted file mode 100644 index 4ff43f4177401..0000000000000 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Data/ProductStockOptionsData.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <!-- Change Maximum Qty Allowed in Shopping Cart config --> - <entity name="ProductStockOptions" type="catalog_inventory_product_stock_options"> - <requiredEntity type="max_qty_to_cart">MaxQtyAllowInCartChange</requiredEntity> - </entity> - <entity name="MaxQtyAllowInCartChange" type="max_qty_to_cart"> - <data key="value">0</data> - </entity> - <!-- Maximum Qty Allowed in Shopping Cart to default config --> - <entity name="DefaultProductStockOptions" type="catalog_inventory_product_stock_options"> - <requiredEntity type="max_qty_to_cart">MaxQtyAllowInCartDefault</requiredEntity> - </entity> - <entity name="MaxQtyAllowInCartDefault" type="max_qty_to_cart"> - <data key="value">10000</data> - </entity> -</entities> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/product_stock_options-meta.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/product_stock_options-meta.xml deleted file mode 100644 index 71b1ebd9806ca..0000000000000 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/product_stock_options-meta.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="CatalogInventoryProductStockSetup" dataType="catalog_inventory_product_stock_options" type="create" auth="adminFormKey" url="/admin/system_config/save/section/cataloginventory/" successRegex="/messages-message-success/" method="POST"> - <object key="groups" dataType="catalog_inventory_product_stock_options"> - <object key="item_options" dataType="catalog_inventory_product_stock_options"> - <object key="fields" dataType="catalog_inventory_product_stock_options"> - <object key="max_sale_qty" dataType="max_qty_to_cart"> - <field key="value">string</field> - </object> - </object> - </object> - </object> - </operation> -</operations> diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json index 1c34d360cf9f1..3086b50168aed 100644 --- a/app/code/Magento/CatalogInventory/composer.json +++ b/app/code/Magento/CatalogInventory/composer.json @@ -14,7 +14,7 @@ "magento/module-sales": "101.0.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index 535e91ba30f52..8c47865da6aa7 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -44,7 +44,7 @@ </type> <type name="Magento\CatalogInventory\Observer\UpdateItemsStockUponConfigChangeObserver"> <arguments> - <argument name="resourceStock" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Proxy</argument> + <argument name="resourceStockItem" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Item\Proxy</argument> </arguments> </type> <type name="Magento\Catalog\Model\Layer"> diff --git a/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php b/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php index 6390822b58f4a..184cb6419294f 100644 --- a/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php +++ b/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php @@ -25,7 +25,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\')', + ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php index 3500506d8d6c5..be8a5a1556193 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php @@ -12,9 +12,14 @@ class Delete extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog { /** * @return void + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $id = $this->getRequest()->getParam('id'); if ($id) { try { diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index 731cbe4531f42..d9e6e338e6d9d 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -7,6 +7,7 @@ namespace Magento\CatalogRule\Model\Indexer; use Magento\Catalog\Model\Product; +use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection; use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory; use Magento\CatalogRule\Model\Rule; use Magento\Framework\App\ObjectManager; @@ -263,14 +264,14 @@ public function reindexByIds(array $ids) */ protected function doReindexByIds($ids) { - $this->cleanByIds($ids); + $this->cleanProductIndex($ids); $products = $this->productLoader->getProducts($ids); - foreach ($this->getActiveRules() as $rule) { - foreach ($products as $product) { - $this->applyRule($rule, $product); - } + $activeRules = $this->getActiveRules(); + foreach ($products as $product) { + $this->applyRules($activeRules, $product); } + $this->reindexRuleGroupWebsite->execute(); } /** @@ -315,6 +316,28 @@ protected function doReindexFull() ); } + /** + * Clean product index + * + * @param array $productIds + */ + private function cleanProductIndex(array $productIds) + { + $where = ['product_id IN (?)' => $productIds]; + $this->connection->delete($this->getTable('catalogrule_product'), $where); + } + + /** + * Clean product price index + * + * @param array $productIds + */ + private function cleanProductPriceIndex(array $productIds) + { + $where = ['product_id IN (?)' => $productIds]; + $this->connection->delete($this->getTable('catalogrule_product_price'), $where); + } + /** * Clean by product ids * @@ -323,51 +346,35 @@ protected function doReindexFull() */ protected function cleanByIds($productIds) { - $query = $this->connection->deleteFromSelect( - $this->connection - ->select() - ->from($this->resource->getTableName('catalogrule_product'), 'product_id') - ->distinct() - ->where('product_id IN (?)', $productIds), - $this->resource->getTableName('catalogrule_product') - ); - $this->connection->query($query); - - $query = $this->connection->deleteFromSelect( - $this->connection->select() - ->from($this->resource->getTableName('catalogrule_product_price'), 'product_id') - ->distinct() - ->where('product_id IN (?)', $productIds), - $this->resource->getTableName('catalogrule_product_price') - ); - $this->connection->query($query); + $this->cleanProductIndex($productIds); + $this->cleanProductPriceIndex($productIds); } /** + * Assign product to rule + * * @param Rule $rule * @param Product $product - * @return $this - * @throws \Exception - * @SuppressWarnings(PHPMD.NPathComplexity) + * @return void */ - protected function applyRule(Rule $rule, $product) + private function assignProductToRule(Rule $rule, Product $product) { - $ruleId = $rule->getId(); - $productEntityId = $product->getId(); - $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); - if (!$rule->validate($product)) { - return $this; + return; } + $ruleId = (int) $rule->getId(); + $productEntityId = (int) $product->getId(); + $ruleProductTable = $this->getTable('catalogrule_product'); $this->connection->delete( - $this->resource->getTableName('catalogrule_product'), + $ruleProductTable, [ - $this->connection->quoteInto('rule_id = ?', $ruleId), - $this->connection->quoteInto('product_id = ?', $productEntityId) + 'rule_id = ?' => $ruleId, + 'product_id = ?' => $productEntityId, ] ); + $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); $customerGroupIds = $rule->getCustomerGroupIds(); $fromTime = strtotime($rule->getFromDate()); $toTime = strtotime($rule->getToDate()); @@ -378,43 +385,62 @@ protected function applyRule(Rule $rule, $product) $actionStop = $rule->getStopRulesProcessing(); $rows = []; - try { - foreach ($websiteIds as $websiteId) { - foreach ($customerGroupIds as $customerGroupId) { - $rows[] = [ - 'rule_id' => $ruleId, - 'from_time' => $fromTime, - 'to_time' => $toTime, - 'website_id' => $websiteId, - 'customer_group_id' => $customerGroupId, - 'product_id' => $productEntityId, - 'action_operator' => $actionOperator, - 'action_amount' => $actionAmount, - 'action_stop' => $actionStop, - 'sort_order' => $sortOrder, - ]; - - if (count($rows) == $this->batchCount) { - $this->connection->insertMultiple($this->getTable('catalogrule_product'), $rows); - $rows = []; - } + foreach ($websiteIds as $websiteId) { + foreach ($customerGroupIds as $customerGroupId) { + $rows[] = [ + 'rule_id' => $ruleId, + 'from_time' => $fromTime, + 'to_time' => $toTime, + 'website_id' => $websiteId, + 'customer_group_id' => $customerGroupId, + 'product_id' => $productEntityId, + 'action_operator' => $actionOperator, + 'action_amount' => $actionAmount, + 'action_stop' => $actionStop, + 'sort_order' => $sortOrder, + ]; + + if (count($rows) == $this->batchCount) { + $this->connection->insertMultiple($ruleProductTable, $rows); + $rows = []; } } - - if (!empty($rows)) { - $this->connection->insertMultiple($this->resource->getTableName('catalogrule_product'), $rows); - } - } catch (\Exception $e) { - throw $e; } + if ($rows) { + $this->connection->insertMultiple($ruleProductTable, $rows); + } + } + /** + * Apply rule + * + * @param Rule $rule + * @param Product $product + * @return $this + * @throws \Exception + */ + protected function applyRule(Rule $rule, $product) + { + $this->assignProductToRule($rule, $product); $this->reindexRuleProductPrice->execute($this->batchCount, $product); $this->reindexRuleGroupWebsite->execute(); return $this; } + private function applyRules(RuleCollection $ruleCollection, Product $product) + { + foreach ($ruleCollection as $rule) { + $this->assignProductToRule($rule, $product); + } + + $this->cleanProductPriceIndex([$product->getId()]); + $this->reindexRuleProductPrice->execute($this->batchCount, $product); + } + /** + * Retrieve table name + * * @param string $tableName * @return string */ @@ -424,6 +450,8 @@ protected function getTable($tableName) } /** + * Update rule product data + * * @param Rule $rule * @return $this * @deprecated 100.2.0 @@ -449,6 +477,8 @@ protected function updateRuleProductData(Rule $rule) } /** + * Apply all rules + * * @param Product|null $product * @throws \Exception * @return $this @@ -488,6 +518,8 @@ protected function deleteOldData() } /** + * Calculate rule product price + * * @param array $ruleData * @param null $productData * @return float @@ -500,6 +532,8 @@ protected function calcRuleProductPrice($ruleData, $productData = null) } /** + * Get rule products statement + * * @param int $websiteId * @param Product|null $product * @return \Zend_Db_Statement_Interface @@ -513,6 +547,8 @@ protected function getRuleProductsStmt($websiteId, Product $product = null) } /** + * Save rule product prices + * * @param array $arrData * @return $this * @throws \Exception @@ -528,7 +564,7 @@ protected function saveRuleProductPrices($arrData) /** * Get active rules * - * @return array + * @return RuleCollection */ protected function getActiveRules() { @@ -538,7 +574,7 @@ protected function getActiveRules() /** * Get active rules * - * @return array + * @return RuleCollection */ protected function getAllRules() { @@ -546,6 +582,8 @@ protected function getAllRules() } /** + * Get product + * * @param int $productId * @return Product */ @@ -558,6 +596,8 @@ protected function getProduct($productId) } /** + * Log critical exception + * * @param \Exception $e * @return void */ diff --git a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php index 537741024c5f9..9ac23e0b9158c 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductPricesPersistor.php @@ -72,25 +72,19 @@ public function execute(array $priceData, $useAdditionalTable = false) ); } - $productIds = []; - - try { - foreach ($priceData as $key => $data) { - $productIds['product_id'] = $data['product_id']; - $priceData[$key]['rule_date'] = $this->dateFormat->formatDate($data['rule_date'], false); - $priceData[$key]['latest_start_date'] = $this->dateFormat->formatDate( - $data['latest_start_date'], - false - ); - $priceData[$key]['earliest_end_date'] = $this->dateFormat->formatDate( - $data['earliest_end_date'], - false - ); - } - $connection->insertOnDuplicate($indexTable, $priceData); - } catch (\Exception $e) { - throw $e; + foreach ($priceData as $key => $data) { + $priceData[$key]['rule_date'] = $this->dateFormat->formatDate($data['rule_date'], false); + $priceData[$key]['latest_start_date'] = $this->dateFormat->formatDate( + $data['latest_start_date'], + false + ); + $priceData[$key]['earliest_end_date'] = $this->dateFormat->formatDate( + $data['earliest_end_date'], + false + ); } + $connection->insertOnDuplicate($indexTable, $priceData); + return true; } } diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index 57578bb0558b7..ebfe91504417c 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -41,7 +41,6 @@ * @method \Magento\CatalogRule\Model\Rule setFromDate(string $value) * @method \Magento\CatalogRule\Model\Rule setToDate(string $value) * @method \Magento\CatalogRule\Model\Rule setCustomerGroupIds(string $value) - * @method string getWebsiteIds() * @method \Magento\CatalogRule\Model\Rule setWebsiteIds(string $value) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) diff --git a/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php b/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php index 6178d51644fde..9b80984628b12 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php @@ -58,7 +58,7 @@ public function testGetButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $deleteUrl . '\')', + ) . '\', \'' . $deleteUrl . '\', {data: {}})', 'sort_order' => 20, ]; diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php index 8252b512e7810..5827bff42b038 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php @@ -144,14 +144,12 @@ protected function setUp() ); $this->ruleCollectionFactory = $this->createPartialMock( \Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory::class, - ['create', 'addFieldToFilter'] + ['create'] ); $this->backend = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class); $this->select = $this->createMock(\Magento\Framework\DB\Select::class); $this->metadataPool = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); - $metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadata::class) - ->disableOriginalConstructor() - ->getMock(); + $metadata = $this->createMock(\Magento\Framework\EntityManager\EntityMetadata::class); $this->metadataPool->expects($this->any())->method('getMetadata')->willReturn($metadata); $this->connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); $this->db = $this->createMock(\Zend_Db_Statement_Interface::class); @@ -181,10 +179,16 @@ protected function setUp() $this->rules->expects($this->any())->method('getWebsiteIds')->will($this->returnValue([1])); $this->rules->expects($this->any())->method('getCustomerGroupIds')->will($this->returnValue([1])); - $this->ruleCollectionFactory->expects($this->any())->method('create')->will($this->returnSelf()); - $this->ruleCollectionFactory->expects($this->any())->method('addFieldToFilter')->will( - $this->returnValue([$this->rules]) - ); + $ruleCollection = $this->createMock(\Magento\CatalogRule\Model\ResourceModel\Rule\Collection::class); + $this->ruleCollectionFactory->expects($this->once()) + ->method('create') + ->willReturn($ruleCollection); + $ruleCollection->expects($this->once()) + ->method('addFieldToFilter') + ->willReturnSelf(); + $ruleIterator = new \ArrayIterator([$this->rules]); + $ruleCollection->method('getIterator') + ->willReturn($ruleIterator); $this->product->expects($this->any())->method('load')->will($this->returnSelf()); $this->product->expects($this->any())->method('getId')->will($this->returnValue(1)); @@ -213,14 +217,12 @@ protected function setUp() ] ); - $this->reindexRuleProductPrice = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::class) - ->disableOriginalConstructor() - ->getMock(); - $this->reindexRuleGroupWebsite = - $this->getMockBuilder(\Magento\CatalogRule\Model\Indexer\ReindexRuleGroupWebsite::class) - ->disableOriginalConstructor() - ->getMock(); + $this->reindexRuleProductPrice = $this->createMock( + \Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::class + ); + $this->reindexRuleGroupWebsite = $this->createMock( + \Magento\CatalogRule\Model\Indexer\ReindexRuleGroupWebsite::class + ); $this->setProperties($this->indexBuilder, [ 'metadataPool' => $this->metadataPool, 'reindexRuleProductPrice' => $this->reindexRuleProductPrice, diff --git a/app/code/Magento/CatalogRule/composer.json b/app/code/Magento/CatalogRule/composer.json index cdcd7629c6040..ef4dece889f6b 100644 --- a/app/code/Magento/CatalogRule/composer.json +++ b/app/code/Magento/CatalogRule/composer.json @@ -17,7 +17,7 @@ "magento/module-catalog-rule-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.6", + "version": "101.0.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php index e61a886a41d6f..e9fb1070fedd5 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php @@ -111,12 +111,9 @@ protected function _getItemsData() $from = ''; } if ($to == '*') { - $to = ''; + $to = null; } - $label = $this->renderRangeLabel( - empty($from) ? 0 : $from, - empty($to) ? 0 : $to - ); + $label = $this->renderRangeLabel(empty($from) ? 0 : $from, $to); $value = $from . '-' . $to; $data[] = [ @@ -141,7 +138,7 @@ protected function _getItemsData() protected function renderRangeLabel($fromPrice, $toPrice) { $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === '') { + if ($toPrice === null) { return __('%1 and above', $formattedFromPrice); } else { if ($fromPrice != $toPrice) { diff --git a/app/code/Magento/CatalogSearch/composer.json b/app/code/Magento/CatalogSearch/composer.json index 0b935c71c17d0..a5f1e5f6fc4f2 100644 --- a/app/code/Magento/CatalogSearch/composer.json +++ b/app/code/Magento/CatalogSearch/composer.json @@ -20,7 +20,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php index 64f032f2d16e9..3e858e96500c5 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php @@ -3,9 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Model; -use Magento\Store\Model\Store; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; class ProductUrlPathGenerator { @@ -19,36 +26,36 @@ class ProductUrlPathGenerator protected $productUrlSuffix = []; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $scopeConfig; /** - * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator + * @var CategoryUrlPathGenerator */ protected $categoryUrlPathGenerator; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var ProductRepositoryInterface */ protected $productRepository; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager + * @param ScopeConfigInterface $scopeConfig * @param CategoryUrlPathGenerator $categoryUrlPathGenerator - * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * @param ProductRepositoryInterface $productRepository */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + StoreManagerInterface $storeManager, + ScopeConfigInterface $scopeConfig, + CategoryUrlPathGenerator $categoryUrlPathGenerator, + ProductRepositoryInterface $productRepository ) { $this->storeManager = $storeManager; $this->scopeConfig = $scopeConfig; @@ -59,8 +66,8 @@ public function __construct( /** * Retrieve Product Url path (with category if exists) * - * @param \Magento\Catalog\Model\Product $product - * @param \Magento\Catalog\Model\Category $category + * @param Product $product + * @param Category $category * * @return string */ @@ -80,10 +87,10 @@ public function getUrlPath($product, $category = null) /** * Prepare URL Key with stored product data (fallback for "Use Default Value" logic) * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return string */ - protected function prepareProductDefaultUrlKey(\Magento\Catalog\Model\Product $product) + protected function prepareProductDefaultUrlKey(Product $product) { $storedProduct = $this->productRepository->getById($product->getId()); $storedUrlKey = $storedProduct->getUrlKey(); @@ -93,9 +100,9 @@ protected function prepareProductDefaultUrlKey(\Magento\Catalog\Model\Product $p /** * Retrieve Product Url path with suffix * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @param int $storeId - * @param \Magento\Catalog\Model\Category $category + * @param Category $category * @return string */ public function getUrlPathWithSuffix($product, $storeId, $category = null) @@ -106,8 +113,8 @@ public function getUrlPathWithSuffix($product, $storeId, $category = null) /** * Get canonical product url path * - * @param \Magento\Catalog\Model\Product $product - * @param \Magento\Catalog\Model\Category|null $category + * @param Product $product + * @param Category|null $category * @return string */ public function getCanonicalUrlPath($product, $category = null) @@ -119,7 +126,7 @@ public function getCanonicalUrlPath($product, $category = null) /** * Generate product url key based on url_key entered by merchant or product name * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return string|null */ public function getUrlKey($product) @@ -131,13 +138,15 @@ public function getUrlKey($product) /** * Prepare url key for product * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return string */ - protected function prepareProductUrlKey(\Magento\Catalog\Model\Product $product) + protected function prepareProductUrlKey(Product $product) { - $urlKey = $product->getUrlKey(); - return $product->formatUrlKey($urlKey === '' || $urlKey === null ? $product->getName() : $urlKey); + $urlKey = (string)$product->getUrlKey(); + $urlKey = trim(strtolower($urlKey)); + + return $urlKey ?: $product->formatUrlKey($product->getName()); } /** @@ -155,7 +164,7 @@ protected function getProductUrlSuffix($storeId = null) if (!isset($this->productUrlSuffix[$storeId])) { $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue( self::XML_PATH_PRODUCT_URL_SUFFIX, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $storeId ); } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php index 956fe1b88e0ad..14f30eb7607d3 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Test\Unit\Model; use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; @@ -72,10 +73,11 @@ protected function setUp() public function getUrlPathDataProvider() { return [ - 'path based on url key' => ['url-key', null, 'url-key'], - 'path based on product name 1' => ['', 'product-name', 'product-name'], - 'path based on product name 2' => [null, 'product-name', 'product-name'], - 'path based on product name 3' => [false, 'product-name', 'product-name'] + 'path based on url key uppercase' => ['Url Key', null, 0, 'url key'], + 'path based on url key' => ['url-key', null, 0, 'url-key'], + 'path based on product name 1' => ['', 'product-name', 1, 'product-name'], + 'path based on product name 2' => [null, 'product-name', 1, 'product-name'], + 'path based on product name 3' => [false, 'product-name', 1, 'product-name'] ]; } @@ -83,15 +85,17 @@ public function getUrlPathDataProvider() * @dataProvider getUrlPathDataProvider * @param string|null|bool $urlKey * @param string|null|bool $productName + * @param int $formatterCalled * @param string $result */ - public function testGetUrlPath($urlKey, $productName, $result) + public function testGetUrlPath($urlKey, $productName, $formatterCalled, $result) { $this->product->expects($this->once())->method('getData')->with('url_path') ->will($this->returnValue(null)); $this->product->expects($this->any())->method('getUrlKey')->will($this->returnValue($urlKey)); $this->product->expects($this->any())->method('getName')->will($this->returnValue($productName)); - $this->product->expects($this->once())->method('formatUrlKey')->will($this->returnArgument(0)); + $this->product->expects($this->exactly($formatterCalled)) + ->method('formatUrlKey')->will($this->returnArgument(0)); $this->assertEquals($result, $this->productUrlPathGenerator->getUrlPath($this->product, null)); } diff --git a/app/code/Magento/CatalogUrlRewrite/composer.json b/app/code/Magento/CatalogUrlRewrite/composer.json index 2f34ee54f36e1..a486b7bb3f6b8 100644 --- a/app/code/Magento/CatalogUrlRewrite/composer.json +++ b/app/code/Magento/CatalogUrlRewrite/composer.json @@ -14,10 +14,10 @@ "magento/module-ui": "101.0.*" }, "suggest": { - "magento/module-webapi": "*" + "magento/module-webapi": "100.2.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogWidget/Setup/UpgradeData.php b/app/code/Magento/CatalogWidget/Setup/UpgradeData.php new file mode 100644 index 0000000000000..5ebdbe2390d51 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Setup/UpgradeData.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogWidget\Setup; + +use Magento\CatalogWidget\Block\Product\ProductsList; +use Magento\CatalogWidget\Model\Rule\Condition\Product as ConditionProduct; +use Magento\Framework\Serialize\Serializer\Json as Serializer; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\UpgradeDataInterface; + +/** + * Upgrade data for CatalogWidget module. + */ +class UpgradeData implements UpgradeDataInterface +{ + /** + * @var Serializer + */ + private $serializer; + + /** + * @param Serializer $serializer + */ + public function __construct( + Serializer $serializer + ) { + $this->serializer = $serializer; + } + + /** + * @inheritdoc + */ + public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + if (version_compare($context->getVersion(), '2.0.1', '<')) { + $this->replaceIsWithIsOneOf($setup); + } + } + + /** + * Replace 'is' condition with 'is one of' in database. + * + * If 'is' product list condition is used with multiple skus it should be replaced by 'is one of' condition. + * + * @param ModuleDataSetupInterface $setup + */ + private function replaceIsWithIsOneOf(ModuleDataSetupInterface $setup) + { + $tableName = $setup->getTable('widget_instance'); + $connection = $setup->getConnection(); + $select = $connection->select() + ->from( + $tableName, + [ + 'instance_id', + 'widget_parameters', + ] + )->where('instance_type = ? ', ProductsList::class); + + $result = $setup->getConnection()->fetchAll($select); + + if ($result) { + $updatedData = $this->updateWidgetData($result); + + $connection->insertOnDuplicate( + $tableName, + $updatedData + ); + } + } + + /** + * Replace 'is' condition with 'is one of' in widget parameters. + * + * @param array $result + * @return array + */ + private function updateWidgetData(array $result): array + { + return array_map( + function ($widgetData) { + $widgetParameters = $this->serializer->unserialize($widgetData['widget_parameters']); + foreach ($widgetParameters['conditions'] as &$condition) { + if (ConditionProduct::class === $condition['type'] && + 'sku' === $condition['attribute'] && + '==' === $condition['operator']) { + $condition['operator'] = '()'; + } + } + $widgetData['widget_parameters'] = $this->serializer->serialize($widgetParameters); + + return $widgetData; + }, + $result + ); + } +} diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json index 7bc1240c43276..510026b008f4e 100644 --- a/app/code/Magento/CatalogWidget/composer.json +++ b/app/code/Magento/CatalogWidget/composer.json @@ -14,7 +14,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CatalogWidget/etc/module.xml b/app/code/Magento/CatalogWidget/etc/module.xml index 8954f11f954f7..1f2d84bef2d6b 100644 --- a/app/code/Magento/CatalogWidget/etc/module.xml +++ b/app/code/Magento/CatalogWidget/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_CatalogWidget" setup_version="2.0.0"> + <module name="Magento_CatalogWidget" setup_version="2.0.1"> <sequence> <module name="Magento_Catalog"/> <module name="Magento_Widget"/> diff --git a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php index bf0884a8c83ea..5a01f524edeb1 100644 --- a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php +++ b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php @@ -274,6 +274,7 @@ protected function getMultilineFieldConfig($attributeCode, array $attributeConfi for ($lineIndex = 0; $lineIndex < (int)$attributeConfig['size']; $lineIndex++) { $isFirstLine = $lineIndex === 0; $line = [ + 'label' => __("%1: Line %2", $attributeConfig['label'], $lineIndex + 1), 'component' => 'Magento_Ui/js/form/element/abstract', 'config' => [ // customScope is used to group elements within a single form e.g. they can be validated separately diff --git a/app/code/Magento/Checkout/Controller/Cart/Add.php b/app/code/Magento/Checkout/Controller/Cart/Add.php index 82085281c93d9..4aebde6b3b0c0 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Add.php +++ b/app/code/Magento/Checkout/Controller/Cart/Add.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -75,10 +74,15 @@ protected function _initProduct() * Add product to shopping cart action * * @return \Magento\Framework\Controller\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + if (!$this->_formKeyValidator->validate($this->getRequest())) { $this->messageManager->addErrorMessage( __('Your session has expired') diff --git a/app/code/Magento/Checkout/Controller/Cart/CouponPost.php b/app/code/Magento/Checkout/Controller/Cart/CouponPost.php index 56215814d2cf6..71407b32f2f22 100644 --- a/app/code/Magento/Checkout/Controller/Cart/CouponPost.php +++ b/app/code/Magento/Checkout/Controller/Cart/CouponPost.php @@ -61,11 +61,16 @@ public function __construct( * Initialize coupon * * @return \Magento\Framework\Controller\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $couponCode = $this->getRequest()->getParam('remove') == 1 ? '' : trim($this->getRequest()->getParam('coupon_code')); @@ -95,14 +100,14 @@ public function execute() if (!$itemsCount) { if ($isCodeLengthValid && $coupon->getId()) { $this->_checkoutSession->getQuote()->setCouponCode($couponCode)->save(); - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __( 'You used coupon code "%1".', $escaper->escapeHtml($couponCode) ) ); } else { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __( 'The coupon code "%1" is not valid.', $escaper->escapeHtml($couponCode) @@ -111,14 +116,14 @@ public function execute() } } else { if ($isCodeLengthValid && $coupon->getId() && $couponCode == $cartQuote->getCouponCode()) { - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __( 'You used coupon code "%1".', $escaper->escapeHtml($couponCode) ) ); } else { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __( 'The coupon code "%1" is not valid.', $escaper->escapeHtml($couponCode) @@ -127,12 +132,12 @@ public function execute() } } } else { - $this->messageManager->addSuccess(__('You canceled the coupon code.')); + $this->messageManager->addSuccessMessage(__('You canceled the coupon code.')); } } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError(__('We cannot apply the coupon code.')); + $this->messageManager->addErrorMessage(__('We cannot apply the coupon code.')); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); } diff --git a/app/code/Magento/Checkout/Controller/Cart/Delete.php b/app/code/Magento/Checkout/Controller/Cart/Delete.php index c0371a6e504f6..98277072fdd99 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Delete.php +++ b/app/code/Magento/Checkout/Controller/Cart/Delete.php @@ -15,7 +15,7 @@ class Delete extends \Magento\Checkout\Controller\Cart */ public function execute() { - if (!$this->_formKeyValidator->validate($this->getRequest())) { + if (!$this->getRequest()->isPost() || !$this->_formKeyValidator->validate($this->getRequest())) { return $this->resultRedirectFactory->create()->setPath('*/*/'); } diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php index 118611263220b..a6eb61169363c 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php @@ -12,11 +12,16 @@ class UpdateItemOptions extends \Magento\Checkout\Controller\Cart * Update product configuration for a cart item * * @return \Magento\Framework\Controller\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $id = (int)$this->getRequest()->getParam('id'); $params = $this->getRequest()->getParams(); diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php index 4ebf39df7defc..90335f8fe164f 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php index c84aec336a589..a17fa44776555 100644 --- a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php +++ b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php @@ -58,7 +58,7 @@ public function __construct( */ public function execute() { - if (!$this->getFormKeyValidator()->validate($this->getRequest())) { + if (!$this->getRequest()->isPost() || !$this->getFormKeyValidator()->validate($this->getRequest())) { return $this->resultRedirectFactory->create()->setPath('*/cart/'); } $itemId = (int)$this->getRequest()->getParam('item_id'); diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index d1894c98e7bce..507411a19f965 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -13,6 +13,8 @@ use Magento\Quote\Model\Quote; /** + * Guest payment information management model. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPaymentInformationManagementInterface @@ -65,7 +67,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa * @param \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement * @param \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory * @param CartRepositoryInterface $cartRepository - * @param ResourceConnection|null + * @param ResourceConnection $connectionPool * @codeCoverageIgnore */ public function __construct( @@ -87,7 +89,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformationAndPlaceOrder( $cartId, @@ -128,7 +130,7 @@ public function savePaymentInformationAndPlaceOrder( } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformation( $cartId, @@ -155,7 +157,7 @@ public function savePaymentInformation( } /** - * {@inheritDoc} + * @inheritdoc */ public function getPaymentInformation($cartId) { @@ -189,9 +191,8 @@ private function limitShippingCarrier(Quote $quote) { $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress && $shippingAddress->getShippingMethod()) { - $shippingDataArray = explode('_', $shippingAddress->getShippingMethod()); - $shippingCarrier = array_shift($shippingDataArray); - $shippingAddress->setLimitCarrier($shippingCarrier); + $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); + $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); } } } diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index d20616b4384d1..09197a90542ee 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -106,4 +106,26 @@ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> </actionGroup> + + <actionGroup name="CheckoutSelectShippingMethodActionGroup"> + <arguments> + <!-- First available shipping method will be selected if value is not passed for shippingMethod --> + <argument name="shippingMethod" defaultValue="" type="string"/> + </arguments> + <conditionalClick selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName(shippingMethod)}}" dependentSelector="{{CheckoutShippingMethodsSection.checkShippingMethodByName(shippingMethod)}}" visible="true" stepKey="selectShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForPaymentMethod"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + </actionGroup> + + <actionGroup name="AssertStorefrontErrorMessageOnOrderSubmit"> + <arguments> + <argument name="errorMessage" type="string"/> + </arguments> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrderNoWait}}" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrderNoWait}}" stepKey="clickPlaceOrder"/> + <waitForText selector="{{StorefrontMessagesSection.errorMessage}}" userInput="{{errorMessage}}" time="30" stepKey="seeShippingMethodError"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml index f03be61cccd3a..c06ff0cb96b58 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GoToCheckoutFromMinicartActionGroup.xml @@ -10,10 +10,11 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!-- Go to checkout from minicart --> <actionGroup name="GoToCheckoutFromMinicartActionGroup"> + <waitForPageLoad stepKey="waitForPageLoad"/> <waitForElement selector="{{StorefrontMinicartSection.showCart}}" stepKey="waitMiniCartSectionShow" /> <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> <waitForElementVisible selector="{{StorefrontMinicartSection.goToCheckout}}" time="30" stepKey="waitForGoToCheckoutButtonVisible"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="clickGoToCheckoutButton"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml index 11cd095334113..7fe9fcb74719d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml @@ -56,4 +56,10 @@ <selectOption selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="" stepKey="clearFieldCounty"/> <clearField selector="{{classPrefix}} {{CheckoutShippingSection.telephone}}" stepKey="clearFieldPhoneNumber"/> </actionGroup> + + <actionGroup name="GuestCheckoutFillNewBillingAddressWithoutEmailActionGroup" + extends="GuestCheckoutFillNewBillingAddressActionGroup"> + <remove keyForRemoval="enterEmail"/> + <selectOption selector="{{CheckoutPaymentSection.country}}" userInput="{{customerAddressVar.country_id}}" stepKey="selectCounty" /> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml index 3390dc588b69b..b3e3c4553d504 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml @@ -35,4 +35,11 @@ <click selector="{{StoreFrontRemoveItemModalSection.ok}}" stepKey="confirmDelete"/> <waitForPageLoad stepKey="waitForDeleteToFinish"/> </actionGroup> + + <!--Check that the minicart is empty--> + <actionGroup name="AssertMiniCartEmpty"> + <dontSeeElement selector="{{StorefrontMinicartSection.productCount}}" stepKey="dontSeeMinicartProductCount"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="expandMinicart"/> + <see selector="{{StorefrontMinicartSection.minicartContent}}" userInput="You have no items in your shopping cart." stepKey="seeEmptyCartMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index ae8f9aa5f2aa7..938d46367e634 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -22,9 +22,11 @@ <element name="guestRegion" type="select" selector=".billing-address-form select[name*='region_id']"/> <element name="guestPostcode" type="input" selector=".billing-address-form input[name*='postcode']"/> <element name="guestTelephone" type="input" selector=".billing-address-form input[name*='telephone']"/> + <element name="country" type="select" selector=".billing-address-form select[name*='country_id']"/> <element name="cartItems" type="text" selector=".minicart-items"/> <element name="billingAddress" type="text" selector="div.billing-address-details"/> <element name="placeOrder" type="button" selector=".payment-method._active button.action.primary.checkout" timeout="30"/> + <element name="placeOrderNoWait" type="button" selector=".payment-method._active button.action.primary.checkout"/> <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> <element name="productItemPriceByName" type="text" selector="//div[@class='product-item-details'][contains(., '{{ProductName}}')]//span[@class='price']" parameterized="true"/> @@ -44,5 +46,8 @@ <element name="checkPaymentMethodByName" type="radio" selector="//div[@id='checkout-payment-method-load']//div[contains(., '{{paymentName}}')]/..//input[@type='radio']" parameterized="true"/> <element name="orderSummaryShippingTotal" type="text" selector=".totals.shipping.excl span.price"/> <element name="noPaymentMethods" type="text" selector=".no-quotes-block"/> + <element name="billingAddressSelectShared" type="select" selector=".checkout-billing-address select[name='billing_address_id']"/> + <element name="billingAddressSameAsShippingShared" type="checkbox" selector="#billing-address-same-as-shipping-shared"/> + <element name="addressAction" type="button" selector="//div[@class='actions-toolbar']//span[text()='{{action}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml new file mode 100644 index 0000000000000..256c59ae05c20 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DefaultBillingAddressShouldBeCheckedOnPaymentPageTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="The default billing address should be used on checkout"/> + <description value="Default billing address should be preselected on payments page on checkout if it exist"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-98974"/> + <useCaseId value="MAGETWO-72961"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Go to Storefront as Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginToStorefrontAccount"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Logout from customer account--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutCustomer"/> + </after> + <!-- Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Click "+ New Address" and Fill new address--> + <click selector="{{CheckoutShippingSection.newAdress}}" stepKey="addAddress"/> + <actionGroup ref="LoggedInUserCheckoutAddNewAddressInShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingAddress"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="US_Address_CA"/> + </actionGroup> + <dontSeeElement selector="{{CheckoutAddressPopupSection.newAddressModalPopup}}" stepKey="dontSeeModalPopup"/> + <!--Select Shipping Rate "Flat Rate" and click "Next" button--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> + <waitForLoadingMaskToDisappear stepKey="waitForShippingMethodMaskDisappear"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + <!--Verify that "My billing and shipping address are the same" is unchecked and billing address is preselected--> + <dontSeeCheckboxIsChecked selector="{{CheckoutPaymentSection.billingAddressSameAsShipping}}" stepKey="shippingAndBillingAddressIsSameUnchecked"/> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="assertBillingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml index a4583fb7fa50c..fef6b9a203735 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -38,20 +38,10 @@ <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" after="seeRegion" stepKey="waitNextButton"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" after="waitNextButton" stepKey="selectShippingMethod"/> <click selector="{{CheckoutShippingMethodsSection.next}}" after="selectShippingMethod" stepKey="clickNextButton" /> - <waitForPageLoad after="clickNextButton" stepKey="waitBillingForm"/> - <seeElement selector="{{CheckoutPaymentSection.isPaymentSection}}" after="waitBillingForm" stepKey="checkoutPaymentStepIsOpened"/> - - <!-- Fill UK Address and verify that payment available and checkout successful --> - <click selector="{{CheckoutShippingSection.newAdress}}" after="shippingStepIsOpened1" stepKey="fillNewAddress" /> - <actionGroup ref="LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup" after="fillNewAddress" stepKey="customerCheckoutFillingShippingSectionUK"> - <argument name="customerVar" value="$$createSimpleUsCustomer$$" /> - <argument name="customerAddressVar" value="UK_Default_Address" /> - </actionGroup> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" after="customerCheckoutFillingShippingSectionUK" stepKey="waitNextButton1"/> - <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" after="waitNextButton1" stepKey="selectShippingMethod1"/> - <click selector="{{CheckoutShippingMethodsSection.next}}" after="selectShippingMethod1" stepKey="clickNextButton1" /> - - <actionGroup ref="CheckoutPlaceOrderActionGroup" after="selectCheckMoneyOrderPayment" stepKey="customerPlaceorder"> + <waitForPageLoad after="clickNextButton" stepKey="waitForPaymentStep"/> + <selectOption selector="{{CheckoutPaymentSection.billingAddressSelectShared}}" userInput="New Address" after="uncheckBillingAddressSameAsShippingCheckCheckBox" stepKey="clickOnNewAddress"/> + <waitForPageLoad stepKey="waitBillingAddressForm"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceorder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml index c80e284633f12..f2c41e0a08763 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml @@ -71,6 +71,7 @@ <!--Refresh Page and Place Order--> <reloadPage stepKey="reloadPage"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="grabOrderNumber"/> @@ -82,7 +83,7 @@ <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="goToOrderReviewPage"/> <see userInput="{{UK_Default_Address.street[0]}} {{UK_Default_Address.city}}, {{UK_Default_Address.postcode}}" selector="{{StorefrontCustomerOrderViewSection.shippingAddress}}" stepKey="checkShippingAddress"/> - <see userInput="{{UK_Default_Address.street[0]}} {{UK_Default_Address.city}}, {{UK_Default_Address.postcode}}" + <see userInput="{{US_Address_TX.street[0]}}" selector="{{StorefrontCustomerOrderViewSection.billingAddress}}" stepKey="checkBillingAddress"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml index 3d1dc2cf66689..49018d6d9df4c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -23,6 +23,7 @@ <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + <magentoCLI command="config:set checkout/options/display_billing_address_on 1" stepKey="setShowBillingAddressOnPaymentPage" /> <magentoCLI command="config:set payment/checkmo/allowspecific" arguments="1" stepKey="setAllowSpecificCountiesValue" /> <magentoCLI command="config:set payment/checkmo/specificcountry" arguments="GB" stepKey="setSpecificCountryValue" /> </before> @@ -31,6 +32,7 @@ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <magentoCLI command="config:set payment/checkmo/allowspecific 0" stepKey="unsetAllowSpecificCountiesValue"/> <magentoCLI command="config:set payment/checkmo/specificcountry ''" stepKey="unsetSpecificCountryValue" /> + <magentoCLI command="config:set checkout/options/display_billing_address_on 0" stepKey="setDisplayBillingAddressOnPaymentMethod" /> </after> <!-- Add product to cart --> @@ -53,14 +55,13 @@ <see userInput="No Payment method available." stepKey="checkMessage"/> <!-- Fill UK Address and verify that payment available and checkout successful --> - <click selector="{{CheckoutHeaderSection.shippingMethodStep}}" stepKey="goToShipping" /> - <waitForElementVisible selector="{{CheckoutShippingSection.isShippingStep}}" stepKey="shippingStepIsOpened1"/> - - <actionGroup ref="GuestCheckoutFillingShippingSectionWithoutRegionActionGroup" stepKey="guestCheckoutFillingShippingSectionUK"> - <argument name="customerVar" value="Simple_US_Customer" /> + <uncheckOption selector="{{CheckoutPaymentSection.billingAddressSameAsShippingShared}}" stepKey="uncheckBillingAddressSameAsShippingCheckCheckBox"/> + <waitForPageLoad stepKey="waitNewAddressBillingForm"/> + <actionGroup ref="GuestCheckoutFillNewBillingAddressWithoutEmailActionGroup" stepKey="guestCheckoutFillingShippingAddress"> + <argument name="customerVar" value="CustomerEntityOne" /> <argument name="customerAddressVar" value="UK_Default_Address" /> - <argument name="shippingMethod" value="Flat Rate" type="string"/> </actionGroup> + <click selector="{{CheckoutPaymentSection.addressAction('Update')}}" stepKey="clickUpdateBillingAddressButton" /> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrderPayment" /> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml index 9fee0302345d5..736b5e66558d0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml @@ -52,6 +52,7 @@ <actionGroup ref="SwitchToTheNewStoreView" stepKey="switchToCustomStoreView"> <argument name="storeViewName" value="customStore"/> </actionGroup> + <scrollToTopOfPage stepKey="scrolToShowNameField"/> <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createProduct.name$$-new" stepKey="fillProductName"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Cart/AddTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/AddTest.php index b06000662e093..f7721bbc58f18 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Cart/AddTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/AddTest.php @@ -58,7 +58,10 @@ public function setUp() $this->resultRedirectFactory = $this->getMockBuilder(RedirectFactory::class) ->disableOriginalConstructor()->getMock(); $this->request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor()->getmock(); + ->disableOriginalConstructor() + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->messageManager = $this->getMockBuilder(ManagerInterface::class) ->disableOriginalConstructor()->getMock(); diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php index b8f46feab0a48..491f4c741e645 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php @@ -85,6 +85,7 @@ class CouponPostTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->response = $this->createMock(\Magento\Framework\App\Response\Http::class); $this->quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, [ 'setCouponCode', @@ -165,15 +166,12 @@ protected function setUp() public function testExecuteWithEmptyCoupon() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn(''); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, ''], + ] + ); $this->cart->expects($this->once()) ->method('getQuote') @@ -184,15 +182,12 @@ public function testExecuteWithEmptyCoupon() public function testExecuteWithGoodCouponAndItems() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn('CODE'); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, 'CODE'], + ] + ); $this->cart->expects($this->any()) ->method('getQuote') @@ -236,7 +231,7 @@ public function testExecuteWithGoodCouponAndItems() ->willReturn('CODE'); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->willReturnSelf(); $this->objectManagerMock->expects($this->once()) @@ -248,15 +243,12 @@ public function testExecuteWithGoodCouponAndItems() public function testExecuteWithGoodCouponAndNoItems() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn('CODE'); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, 'CODE'], + ] + ); $this->cart->expects($this->any()) ->method('getQuote') @@ -290,7 +282,7 @@ public function testExecuteWithGoodCouponAndNoItems() ->willReturnSelf(); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->willReturnSelf(); $this->objectManagerMock->expects($this->once()) @@ -302,15 +294,12 @@ public function testExecuteWithGoodCouponAndNoItems() public function testExecuteWithBadCouponAndItems() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn(''); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, ''], + ] + ); $this->cart->expects($this->any()) ->method('getQuote') @@ -344,7 +333,7 @@ public function testExecuteWithBadCouponAndItems() ->willReturnSelf(); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with('You canceled the coupon code.') ->willReturnSelf(); @@ -353,15 +342,12 @@ public function testExecuteWithBadCouponAndItems() public function testExecuteWithBadCouponAndNoItems() { - $this->request->expects($this->at(0)) - ->method('getParam') - ->with('remove') - ->willReturn(0); - - $this->request->expects($this->at(1)) - ->method('getParam') - ->with('coupon_code') - ->willReturn('CODE'); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['remove', null, 0], + ['coupon_code', null, 'CODE'], + ] + ); $this->cart->expects($this->any()) ->method('getQuote') @@ -386,7 +372,7 @@ public function testExecuteWithBadCouponAndNoItems() ->willReturn($coupon); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->willReturnSelf(); $this->objectManagerMock->expects($this->once()) diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php index 7653a51b2f9b7..3f9a84c1b1763 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php @@ -47,7 +47,11 @@ protected function setUp() $this->sidebarMock = $this->createMock(\Magento\Checkout\Model\Sidebar::class); $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); $this->jsonHelperMock = $this->createMock(\Magento\Framework\Json\Helper\Data::class); - $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->responseMock = $this->getMockForAbstractClass( \Magento\Framework\App\ResponseInterface::class, [], diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index 2d313d2f50052..7a731c1c07039 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php @@ -280,9 +280,11 @@ private function getMockForAssignBillingAddress($cartId, $billingAddressMock) $billingAddressId = 1; $quote = $this->createMock(Quote::class); $quoteBillingAddress = $this->createMock(Address::class); + $shippingRate = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Rate::class, []); + $shippingRate->setCarrier('flatrate'); $quoteShippingAddress = $this->createPartialMock( Address::class, - ['setLimitCarrier', 'getShippingMethod'] + ['setLimitCarrier', 'getShippingMethod', 'getShippingRateByCode'] ); $this->cartRepositoryMock->method('getActive') ->with($cartId) @@ -302,6 +304,9 @@ private function getMockForAssignBillingAddress($cartId, $billingAddressMock) $quote->expects($this->once()) ->method('setBillingAddress') ->with($billingAddressMock); + $quoteShippingAddress->expects($this->any()) + ->method('getShippingRateByCode') + ->willReturn($shippingRate); $quote->expects($this->once()) ->method('setDataChanges') ->willReturnSelf(); diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index 0fbfaffa7f22a..c46aaa9cb612c 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -26,7 +26,7 @@ "magento/module-cookie": "100.2.*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml index 71dfd12bb4779..4ebd594a28562 100644 --- a/app/code/Magento/Checkout/etc/di.xml +++ b/app/code/Magento/Checkout/etc/di.xml @@ -49,7 +49,4 @@ </argument> </arguments> </type> - <type name="Magento\Quote\Model\Quote"> - <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> - </type> </config> diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index 00bcd2a27005a..8f35fe9f37abf 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -96,4 +96,7 @@ </argument> </arguments> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> + </type> </config> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 1005c11e44d95..84ab9b13d8f3a 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -14,7 +14,8 @@ method="post" id="form-validate" data-mage-init='{"Magento_Checkout/js/action/update-shopping-cart": - {"validationURL" : "/checkout/cart/updateItemQty"} + {"validationURL" : "/checkout/cart/updateItemQty", + "updateCartActionContainer": "#update_cart_action_container"} }' class="form form-cart"> <?= $block->getBlockHtml('formkey') ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml index c96df9cdd3195..454031279d882 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml @@ -49,7 +49,7 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima <?php if (isset($_formatedOptionValue['full_view'])): ?> <?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?> <?php else: ?> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['span']) ?> <?php endif; ?> </dd> <?php endforeach; ?> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js index ce1527b3d72d6..1920bc4d7ac41 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js @@ -14,7 +14,8 @@ define([ $.widget('mage.updateShoppingCart', { options: { validationURL: '', - eventName: 'updateCartItemQty' + eventName: 'updateCartItemQty', + updateCartActionContainer: '' }, /** @inheritdoc */ @@ -31,7 +32,9 @@ define([ * @return {Boolean} */ onSubmit: function (event) { - if (!this.options.validationURL) { + var action = this.element.find(this.options.updateCartActionContainer).val(); + + if (!this.options.validationURL || action === 'empty_cart') { return true; } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js index bf152f68e25e5..e54f464f24d02 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js @@ -60,7 +60,6 @@ define([ this.resolveBillingAddress(); } } - }, /** @@ -244,7 +243,7 @@ define([ return; } - if (quote.isVirtual()) { + if (quote.isVirtual() || !quote.billingAddress()) { isBillingAddressInitialized = addressList.some(function (addrs) { if (addrs.isDefaultBilling()) { selectBillingAddress(addrs); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js index 2510d1aced3d3..3486a92736617 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js @@ -7,7 +7,8 @@ */ define([ 'ko', - 'underscore' + 'underscore', + 'domReady!' ], function (ko, _) { 'use strict'; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js index 447d626b339bd..e34f861f7714f 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-save-processor/default.js @@ -35,8 +35,9 @@ define([ saveShippingInformation: function () { var payload; - /* Assign selected address every time buyer selects address*/ - selectBillingAddressAction(quote.shippingAddress()); + if (!quote.billingAddress() && quote.shippingAddress().canUseForBilling()) { + selectBillingAddressAction(quote.shippingAddress()); + } payload = { addressInformation: { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index 399321bd2f67d..8935242724f3e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -14,7 +14,11 @@ define([ _create: function () { var items, i; - $(this.options.emptyCartButton).on('click', $.proxy(function () { + $(this.options.emptyCartButton).on('click', $.proxy(function (event) { + if (event.detail === 0) { + return; + } + $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); $(this.options.updateCartActionContainer) .attr('name', 'update_cart_action').attr('value', 'empty_cart'); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js index c17e5e40d5c98..e8994c61b7221 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js @@ -66,9 +66,21 @@ define([ navigate: function () { var self = this; - getPaymentInformation().done(function () { - self.isVisible(true); - }); + if (!self.hasShippingMethod()) { + this.isVisible(false); + stepNavigator.setHash('shipping'); + } else { + getPaymentInformation().done(function () { + self.isVisible(true); + }); + } + }, + + /** + * @return {Boolean} + */ + hasShippingMethod: function () { + return window.checkoutConfig.selectedShippingMethod !== null; }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html index 357b0e550af0f..41d442a76d510 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html @@ -45,7 +45,7 @@ <span data-bind="html: option.value.join('<br>')"></span> <!-- /ko --> <!-- ko ifnot: Array.isArray(option.value) --> - <span data-bind="html: option.value"></span> + <span data-bind="text: option.value"></span> <!-- /ko --> </dd> <!-- /ko --> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html index 2491ee12d263c..9c0621099060b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html @@ -35,7 +35,7 @@ <dd class="values" data-bind="html: full_view"></dd> <!-- /ko --> <!-- ko ifnot: ($data.full_view)--> - <dd class="values" data-bind="html: value"></dd> + <dd class="values" data-bind="text: value"></dd> <!-- /ko --> <!-- /ko --> </dl> diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php index f7b178df99624..447689c95dfd0 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php @@ -9,6 +9,7 @@ use Magento\CheckoutAgreements\Api\CheckoutAgreementsRepositoryInterface; use Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; use Magento\Backend\App\Action\Context; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Registry; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; @@ -36,9 +37,14 @@ public function __construct( } /** * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $id = (int)$this->getRequest()->getParam('id'); $agreement = $this->agreementRepository->get($id); if (!$agreement->getAgreementId()) { diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php index 331307292e40a..61326207d24ec 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php @@ -45,17 +45,18 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { $agreements = []; $agreements['checkoutAgreements'] = $this->getAgreementsConfig(); + return $agreements; } /** - * Returns agreements config + * Returns agreements config. * * @return array */ @@ -75,7 +76,7 @@ protected function getAgreementsConfig() 'content' => $agreement->getIsHtml() ? $agreement->getContent() : nl2br($this->escaper->escapeHtml($agreement->getContent())), - 'checkboxText' => $agreement->getCheckboxText(), + 'checkboxText' => $this->escaper->escapeHtml($agreement->getCheckboxText()), 'mode' => $agreement->getMode(), 'agreementId' => $agreement->getAgreementId() ]; diff --git a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php index eae347e27aa11..cacc1c1226cff 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php +++ b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php @@ -8,6 +8,9 @@ use Magento\CheckoutAgreements\Model\AgreementsProvider; use Magento\Store\Model\ScopeInterface; +/** + * Tests for AgreementsConfigProvider. + */ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase { /** @@ -30,6 +33,9 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase */ protected $escaperMock; + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); @@ -45,10 +51,16 @@ protected function setUp() ); } + /** + * Test for getConfig if content is HTML. + * + * @return void + */ public function testGetConfigIfContentIsHtml() { $content = 'content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -57,12 +69,12 @@ public function testGetConfigIfContentIsHtml() 'agreements' => [ [ 'content' => $content, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -71,8 +83,12 @@ public function testGetConfigIfContentIsHtml() ->willReturn(true); $agreement = $this->createMock(\Magento\CheckoutAgreements\Api\Data\AgreementInterface::class); - $this->agreementsRepositoryMock->expects($this->any())->method('getList')->willReturn([$agreement]); + $this->agreementsRepositoryMock->expects($this->once())->method('getList')->willReturn([$agreement]); + $this->escaperMock->expects($this->once()) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); $agreement->expects($this->once())->method('getIsHtml')->willReturn(true); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); @@ -82,11 +98,17 @@ public function testGetConfigIfContentIsHtml() $this->assertEquals($expectedResult, $this->model->getConfig()); } + /** + * Test for getConfig if content is not HTML. + * + * @return void + */ public function testGetConfigIfContentIsNotHtml() { $content = 'content'; $escapedContent = 'escaped_content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -95,12 +117,12 @@ public function testGetConfigIfContentIsNotHtml() 'agreements' => [ [ 'content' => $escapedContent, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -109,9 +131,13 @@ public function testGetConfigIfContentIsNotHtml() ->willReturn(true); $agreement = $this->createMock(\Magento\CheckoutAgreements\Api\Data\AgreementInterface::class); - $this->agreementsRepositoryMock->expects($this->any())->method('getList')->willReturn([$agreement]); - $this->escaperMock->expects($this->once())->method('escapeHtml')->with($content)->willReturn($escapedContent); + $this->agreementsRepositoryMock->expects($this->once())->method('getList')->willReturn([$agreement]); + $this->escaperMock->expects($this->at(0))->method('escapeHtml')->with($content)->willReturn($escapedContent); + $this->escaperMock->expects($this->at(1)) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); $agreement->expects($this->once())->method('getIsHtml')->willReturn(false); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); diff --git a/app/code/Magento/CheckoutAgreements/composer.json b/app/code/Magento/CheckoutAgreements/composer.json index c6c2102600974..93ecb3bf836ae 100644 --- a/app/code/Magento/CheckoutAgreements/composer.json +++ b/app/code/Magento/CheckoutAgreements/composer.json @@ -10,7 +10,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml index 3f742de0177da..122160f1a10cd 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout_overview"> - <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::multishipping_agreements.phtml"/> + <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::additional_agreements.phtml"/> </referenceBlock> </body> </page> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml index 3400770f5cee8..33227f0cdce3c 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html index a448537d64e83..4b1a68624e547 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html @@ -5,17 +5,17 @@ */ --> <div data-role="checkout-agreements"> - <div class="checkout-agreements" data-bind="visible: isVisible"> + <div class="checkout-agreements fieldset" data-bind="visible: isVisible"> <!-- ko foreach: agreements --> <!-- ko if: ($parent.isAgreementRequired($data)) --> - <div class="checkout-agreement required"> + <div class="checkout-agreement field choice required"> <input type="checkbox" class="required-entry" data-bind="attr: { 'id': $parent.getCheckboxId($parentContext, agreementId), 'name': 'agreement[' + agreementId + ']', 'value': agreementId }"/> - <label data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> + <label class="label" data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> <button type="button" class="action action-show" data-bind="click: function(data, event) { return $parent.showContent(data, event) }" diff --git a/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php b/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php index a7410cac64d76..926886316f16a 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php @@ -24,7 +24,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->getDeleteUrl() . '\')', + ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php b/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php index 1fc599e4c856a..b11c2fc4163b2 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php @@ -24,7 +24,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->getDeleteUrl() . '\')', + ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/Cms/Block/Block.php b/app/code/Magento/Cms/Block/Block.php index d0d75ea691195..c611f4b1e9f05 100644 --- a/app/code/Magento/Cms/Block/Block.php +++ b/app/code/Magento/Cms/Block/Block.php @@ -84,4 +84,14 @@ public function getIdentities() { return [\Magento\Cms\Model\Block::CACHE_TAG . '_' . $this->getBlockId()]; } + + /** + * @inheritdoc + */ + public function getCacheKeyInfo() + { + $cacheKeyInfo = parent::getCacheKeyInfo(); + $cacheKeyInfo[] = $this->_storeManager->getStore()->getId(); + return $cacheKeyInfo; + } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php index 3aaf40e7d0ab2..22672e57ee6ab 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php @@ -12,9 +12,14 @@ class Delete extends \Magento\Cms\Controller\Adminhtml\Block * Delete action * * @return \Magento\Framework\Controller\ResultInterface + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); // check if we know what should be deleted diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php index 92bc7ad71f590..ef0fa937dbd5c 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php @@ -49,10 +49,14 @@ public function __construct(Context $context, Filter $filter, CollectionFactory * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); $collectionSize = $collection->getSize(); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php index 16c99e9857c33..78753aee66cef 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -19,9 +18,14 @@ class Delete extends \Magento\Backend\App\Action * Delete action * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + // check if we know what should be deleted $id = $this->getRequest()->getParam('page_id'); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php index a1d32aa97a382..6d8fda918689d 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php @@ -48,10 +48,14 @@ public function __construct(Context $context, Filter $filter, CollectionFactory * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); $collectionSize = $collection->getSize(); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php index a85b8ecd5e5a1..b36cf087ed9d6 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php @@ -48,10 +48,14 @@ public function __construct(Context $context, Filter $filter, CollectionFactory * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); foreach ($collection as $item) { diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php index 3f26769e4c9e9..5ab7ae246a76f 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php @@ -48,10 +48,14 @@ public function __construct(Context $context, Filter $filter, CollectionFactory * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); foreach ($collection as $item) { diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php index 1364e61816796..42b5c8f8497ec 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php @@ -44,8 +44,8 @@ class Save extends \Magento\Backend\App\Action * @param Action\Context $context * @param PostDataProcessor $dataProcessor * @param DataPersistorInterface $dataPersistor - * @param \Magento\Cms\Model\PageFactory $pageFactory - * @param \Magento\Cms\Api\PageRepositoryInterface $pageRepository + * @param \Magento\Cms\Model\PageFactory|null $pageFactory + * @param \Magento\Cms\Api\PageRepositoryInterface|null $pageRepository */ public function __construct( Action\Context $context, diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php index a1de11c3c462e..81ae1affb5e00 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php @@ -7,6 +7,7 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NotFoundException; /** * Delete image folder. @@ -57,6 +58,10 @@ public function __construct( */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { $path = $this->getStorage()->getCmsWysiwygImages()->getCurrentPath(); if (!$this->directoryResolver->validatePath($path, DirectoryList::MEDIA)) { diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php index 7816e29405f27..5171430e67371 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php @@ -7,6 +7,7 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NotFoundException; /** * Creates new folder. @@ -28,7 +29,6 @@ class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory * @param \Magento\Framework\App\Filesystem\DirectoryResolver|null $directoryResolver - * @throws \RuntimeException */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -50,6 +50,10 @@ public function __construct( */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { $this->_initAction(); $name = $this->getRequest()->getPost('name'); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php index 5c9aa2243bc6d..b25ad695ba008 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php @@ -50,6 +50,10 @@ public function __construct( public function execute() { try { + if (!$this->getRequest()->isPost()) { + throw new \Exception('Wrong request.'); + } + $this->_initAction(); $path = $this->getStorage()->getSession()->getCurrentPath(); if (!$this->directoryResolver->validatePath($path, DirectoryList::MEDIA)) { diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 2cd1647a1bf22..90dcf3dc8df78 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -270,7 +270,8 @@ public function getDirsCollection($path) $collection = $this->getCollection($path) ->setCollectDirs(true) ->setCollectFiles(false) - ->setCollectRecursively(false); + ->setCollectRecursively(false) + ->setOrder('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC); $conditions = $this->getConditionsForExcludeDirs(); diff --git a/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml index c8f71253dc6bd..79da00f26ecb9 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/CmsBlockData.xml @@ -14,4 +14,10 @@ <data key="content">Here is a block test. Yeah!</data> <data key="active">true</data> </entity> + <entity name="Sales25offBlock" type="cms_block"> + <data key="title" unique="suffix">Sales25off</data> + <data key="identifier" unique="suffix">Sales25off</data> + <data key="content">sales25off everything!</data> + <data key="active">false</data> + </entity> </entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml b/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml index bab2be6a36155..60a33c132a6c1 100644 --- a/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml +++ b/app/code/Magento/Cms/Test/Mftf/Metadata/cms_block-meta.xml @@ -13,7 +13,7 @@ <field key="title">string</field> <field key="identifier">string</field> <field key="content">string</field> - <field key="active">true</field> + <field key="active">string</field> </object> </operation> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml index e3584427f4767..328dc156a38fb 100644 --- a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockEditPage.xml @@ -8,7 +8,7 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="AdminCmsBlockEditPage" url="/cms/block/edit/id/{{var1}}" area="admin" module="Magento_Cms" parameterized="true"> + <page name="AdminCmsBlockEditPage" url="/cms/block/edit/block_id/{{blockId}}/" area="admin" module="Magento_Cms" parameterized="true"> <section name="AdminCmsBlockContentSection" /> <section name="AdminMediaGallerySection" /> </page> diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/DeleteTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/DeleteTest.php index 55e8382d9ca23..c11c7c3810832 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/DeleteTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/DeleteTest.php @@ -73,7 +73,7 @@ protected function setUp() false, true, true, - ['getParam'] + ['getParam', 'isPost'] ); $this->blockMock = $this->getMockBuilder(\Magento\Cms\Model\Block::class) @@ -110,6 +110,8 @@ protected function setUp() ->method('getResultRedirectFactory') ->willReturn($this->resultRedirectFactoryMock); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->deleteController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Block\Delete::class, [ diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/MassDeleteTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/MassDeleteTest.php index 39a7d0d74e4d8..3088e3b62c364 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/MassDeleteTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Block/MassDeleteTest.php @@ -36,12 +36,16 @@ protected function setUp() $this->blockCollectionMock = $this->createMock(\Magento\Cms\Model\ResourceModel\Block\Collection::class); + $requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($requestMock); + $this->massDeleteController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Block\MassDelete::class, [ 'context' => $this->contextMock, 'filter' => $this->filterMock, - 'collectionFactory' => $this->collectionFactoryMock + 'collectionFactory' => $this->collectionFactoryMock, ] ); } diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/DeleteTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/DeleteTest.php index 09b36bc41d405..48098242197ae 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/DeleteTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/DeleteTest.php @@ -56,7 +56,7 @@ protected function setUp() false, true, true, - ['getParam'] + ['getParam', 'isPost'] ); $this->pageMock = $this->getMockBuilder(\Magento\Cms\Model\Page::class) @@ -95,6 +95,8 @@ protected function setUp() ->method('getResultRedirectFactory') ->willReturn($this->resultRedirectFactoryMock); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->deleteController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Page\Delete::class, [ diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDeleteTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDeleteTest.php index f51ab152ba2a4..c0f3a719091d7 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDeleteTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDeleteTest.php @@ -36,12 +36,16 @@ protected function setUp() $this->pageCollectionMock = $this->createMock(\Magento\Cms\Model\ResourceModel\Page\Collection::class); + $requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($requestMock); + $this->massDeleteController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Page\MassDelete::class, [ 'context' => $this->contextMock, 'filter' => $this->filterMock, - 'collectionFactory' => $this->collectionFactoryMock + 'collectionFactory' => $this->collectionFactoryMock, ] ); } diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDisableTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDisableTest.php index 5b80dd1873d5c..64b47b5a08416 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDisableTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassDisableTest.php @@ -35,6 +35,10 @@ protected function setUp() $this->pageCollectionMock = $this->createMock(\Magento\Cms\Model\ResourceModel\Page\Collection::class); + $requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($requestMock); + $this->massDisableController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Page\MassDisable::class, [ diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassEnableTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassEnableTest.php index 16b3dfe4ee638..a63a81882dfe9 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassEnableTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Page/MassEnableTest.php @@ -35,6 +35,10 @@ protected function setUp() $this->pageCollectionMock = $this->createMock(\Magento\Cms\Model\ResourceModel\Page\Collection::class); + $requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $requestMock->expects($this->any())->method('isPost')->willReturn(true); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($requestMock); + $this->massEnableController = $this->objectManager->getObject( \Magento\Cms\Controller\Adminhtml\Page\MassEnable::class, [ diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index 906a7d4fbc605..20c0b2075f5c3 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -414,6 +414,10 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e ->method('setCollectRecursively') ->with(false) ->willReturnSelf(); + $storageCollectionMock->expects($this->once()) + ->method('setOrder') + ->with('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC) + ->willReturnSelf(); $storageCollectionMock->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator($collectionArray)); diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php index 3dcf6c4a3fce0..3095abef7bbe3 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php @@ -95,6 +95,7 @@ public function testPrepareDataSource() 'title' => __('Delete %1', $title), 'message' => __('Are you sure you want to delete a %1 record?', $title) ], + 'post' => true ] ], ] diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php index b0cc1bf061a48..9b3165a2c5517 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php @@ -70,6 +70,7 @@ public function testPrepareItemsByPageId() 'title' => __('Delete %1', $title), 'message' => __('Are you sure you want to delete a %1 record?', $title) ], + 'post' => true ] ], ] diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php index 60b9f34d29ae6..30b966c6a8610 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php @@ -87,7 +87,8 @@ public function prepareDataSource(array $dataSource) 'confirm' => [ 'title' => __('Delete %1', $title), 'message' => __('Are you sure you want to delete a %1 record?', $title) - ] + ], + 'post' => true, ] ]; } @@ -99,6 +100,7 @@ public function prepareDataSource(array $dataSource) /** * Get instance of escaper + * * @return Escaper * @deprecated 101.0.7 */ diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php index ea6882e21c85f..f7c16e2065c47 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php @@ -89,7 +89,8 @@ public function prepareDataSource(array $dataSource) 'confirm' => [ 'title' => __('Delete %1', $title), 'message' => __('Are you sure you want to delete a %1 record?', $title) - ] + ], + 'post' => true, ]; } if (isset($item['identifier'])) { @@ -110,6 +111,7 @@ public function prepareDataSource(array $dataSource) /** * Get instance of escaper + * * @return Escaper * @deprecated 101.0.7 */ diff --git a/app/code/Magento/Cms/composer.json b/app/code/Magento/Cms/composer.json index 3f425e91b89e2..d463cedd8dcd2 100644 --- a/app/code/Magento/Cms/composer.json +++ b/app/code/Magento/Cms/composer.json @@ -18,7 +18,7 @@ "magento/module-cms-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "102.0.7", + "version": "102.0.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml index 9f886f6f1345e..793fc7d26cb4a 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml @@ -146,7 +146,6 @@ <editor> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> - <rule name="validate-xml-identifier" xsi:type="boolean">true</rule> </validation> <editorType>text</editorType> </editor> diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php index a05151153daa2..63a2b811f93ec 100644 --- a/app/code/Magento/Config/Block/System/Config/Form.php +++ b/app/code/Magento/Config/Block/System/Config/Form.php @@ -423,6 +423,10 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie $backendModel = $field->getBackendModel(); // Backend models which implement ProcessorInterface are processed by ScopeConfigInterface if (!$backendModel instanceof ProcessorInterface) { + if (array_key_exists($path, $this->_configData)) { + $data = $this->_configData[$path]; + } + $backendModel->setPath($path) ->setValue($data) ->setWebsite($this->getWebsiteCode()) diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php index 7f7d461ea090d..893a73654137e 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php @@ -6,6 +6,7 @@ namespace Magento\Config\Controller\Adminhtml\System\Config; use Magento\Config\Controller\Adminhtml\System\AbstractConfig; +use Magento\Framework\Exception\NotFoundException; /** * System Configuration Save Controller @@ -140,9 +141,14 @@ protected function _saveAdvanced() * Save configuration * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { // custom save logic $this->_saveSection(); diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index b1074e92cc949..6bf191c20a844 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -424,6 +424,11 @@ protected function _processGroup( if (!isset($fieldData['value'])) { $fieldData['value'] = null; } + + if ($field->getType() == 'multiline' && is_array($fieldData['value'])) { + $fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value'])); + } + $data = [ 'field' => $fieldId, 'groups' => $groups, @@ -520,24 +525,29 @@ public function setDataByPath($path, $value) if ($path === '') { throw new \UnexpectedValueException('Path must not be empty'); } + $pathParts = explode('/', $path); $keyDepth = count($pathParts); - if ($keyDepth !== 3) { + if ($keyDepth < 3) { throw new \UnexpectedValueException( - "Allowed depth of configuration is 3 (<section>/<group>/<field>). Your configuration depth is " - . $keyDepth . " for path '$path'" + 'Minimal depth of configuration is 3. Your configuration depth is ' . $keyDepth ); } + + $section = array_shift($pathParts); $data = [ - 'section' => $pathParts[0], - 'groups' => [ - $pathParts[1] => [ - 'fields' => [ - $pathParts[2] => ['value' => $value], - ], - ], + 'fields' => [ + array_pop($pathParts) => ['value' => $value], ], ]; + while ($pathParts) { + $data = [ + 'groups' => [ + array_pop($pathParts) => $data, + ], + ]; + } + $data['section'] = $section; $this->addData($data); } diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php index 9a483de6a695b..d12569eebe5b2 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php +++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php @@ -56,8 +56,9 @@ public function beforeSave() { $value = $this->getValue(); if ($value == 1) { - $customUrl = $this->getData('groups/url/fields/custom/value'); - if (empty($customUrl)) { + $customUrlField = $this->getData('groups/url/fields/custom/value'); + $customUrlConfig = $this->_config->getValue('admin/url/custom'); + if (empty($customUrlField) && empty($customUrlConfig)) { throw new \Magento\Framework\Exception\LocalizedException(__('Please specify the admin custom URL.')); } } diff --git a/app/code/Magento/Config/Model/Config/Backend/Serialized.php b/app/code/Magento/Config/Model/Config/Backend/Serialized.php index 3d5713357c39c..4d5da764db470 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Serialized.php +++ b/app/code/Magento/Config/Model/Config/Backend/Serialized.php @@ -52,7 +52,18 @@ protected function _afterLoad() { $value = $this->getValue(); if (!is_array($value)) { - $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); + try { + $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); + } catch (\Exception $e) { + $this->_logger->critical( + sprintf( + 'Failed to unserialize %s config value. The error is: %s', + $this->getPath(), + $e->getMessage() + ) + ); + $this->setValue(false); + } } } diff --git a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php index b3474674cf76d..5beff0d043ade 100644 --- a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php +++ b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Locale currency source - */ namespace Magento\Config\Model\Config\Source\Locale; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Locale\ListsInterface; + /** + * Locale currency source. + * * @api * @since 100.0.2 */ @@ -21,27 +24,70 @@ class Currency implements \Magento\Framework\Option\ArrayInterface protected $_options; /** - * @var \Magento\Framework\Locale\ListsInterface + * @var ListsInterface */ protected $_localeLists; /** - * @param \Magento\Framework\Locale\ListsInterface $localeLists + * @var ScopeConfigInterface */ - public function __construct(\Magento\Framework\Locale\ListsInterface $localeLists) - { + private $config; + + /** + * @var array + */ + private $installedCurrencies; + + /** + * @param ListsInterface $localeLists + * @param ScopeConfigInterface $config + */ + public function __construct( + ListsInterface $localeLists, + ScopeConfigInterface $config = null + ) { $this->_localeLists = $localeLists; + $this->config = $config ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** - * @return array + * @inheritdoc */ public function toOptionArray() { if (!$this->_options) { $this->_options = $this->_localeLists->getOptionCurrencies(); } - $options = $this->_options; + + $selected = array_flip($this->getInstalledCurrencies()); + + $options = array_filter( + $this->_options, + function ($option) use ($selected) { + return isset($selected[$option['value']]); + } + ); + return $options; } + + /** + * Retrieve Installed Currencies. + * + * @return array + */ + private function getInstalledCurrencies() + { + if (!$this->installedCurrencies) { + $this->installedCurrencies = explode( + ',', + $this->config->getValue( + 'system/currency/installed', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + ); + } + + return $this->installedCurrencies; + } } diff --git a/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml new file mode 100644 index 0000000000000..e998730d11ae7 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SetLocaleOptions" type="locale_options_config"> + <requiredEntity type="code">LocaleOptionsFrance</requiredEntity> + </entity> + <entity name="LocaleOptionsFrance" type="code"> + <data key="value">fr_FR</data> + </entity> + + <entity name="DefaultLocaleOptions" type="locale_options_config"> + <requiredEntity type="code">LocaleOptionsUSA</requiredEntity> + </entity> + <entity name="LocaleOptionsUSA" type="code"> + <data key="value">en_US</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml new file mode 100644 index 0000000000000..6398d51cda916 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="GeneralLocaleOptionsConfig" dataType="locale_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="locale_options_config"> + <object key="locale" dataType="locale_options_config"> + <object key="fields" dataType="locale_options_config"> + <object key="code" dataType="code"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php index de18d45d26864..011bcfee64af5 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php @@ -11,6 +11,11 @@ class FileTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\Config\Block\System\Config\Form\Field\File */ @@ -24,6 +29,8 @@ class FileTest extends \PHPUnit\Framework\TestCase protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->escaperMock->method('escapeHtml')->willReturnArgument(0); $this->testData = [ 'before_element_html' => 'test_before_element_html', @@ -40,7 +47,10 @@ protected function setUp() $this->file = $objectManager->getObject( \Magento\Config\Block\System\Config\Form\Field\File::class, - ['data' => $this->testData] + [ + 'escaper' => $this->escaperMock, + 'data' => $this->testData + ] ); $formMock = new \Magento\Framework\DataObject(); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php index e62aa37af47dc..6f771a2e38078 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php @@ -13,6 +13,11 @@ class ImageTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\Framework\Url|\PHPUnit_Framework_MockObject_MockObject */ @@ -31,10 +36,13 @@ class ImageTest extends \PHPUnit\Framework\TestCase protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->escaperMock->method('escapeHtml')->willReturnArgument(0); $this->urlBuilderMock = $this->createMock(\Magento\Framework\Url::class); $this->image = $objectManager->getObject( \Magento\Config\Block\System\Config\Form\Field\Image::class, [ + 'escaper' => $this->escaperMock, 'urlBuilder' => $this->urlBuilderMock, ] ); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php index f5c65e848b3bf..3799136aea9c0 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php @@ -7,6 +7,11 @@ class AllowspecificTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific */ @@ -20,8 +25,11 @@ class AllowspecificTest extends \PHPUnit\Framework\TestCase protected function setUp() { $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->escaperMock->method('escapeHtml')->willReturnArgument(0); $this->_object = $testHelper->getObject( - \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class + \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class, + ['escaper' => $this->escaperMock] ); $this->_object->setData('html_id', 'spec_element'); $this->_formMock = $this->createPartialMock( diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php index 069a1c20b2966..980d8355de555 100644 --- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php +++ b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php @@ -69,6 +69,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->_requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->_requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->_responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); $configStructureMock = $this->createMock(\Magento\Config\Model\Config\Structure::class); diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php index bb1e0e0225901..048df95f98649 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Model\Context; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Psr\Log\LoggerInterface; class SerializedTest extends \PHPUnit\Framework\TestCase { @@ -18,14 +19,20 @@ class SerializedTest extends \PHPUnit\Framework\TestCase /** @var Json|\PHPUnit_Framework_MockObject_MockObject */ private $serializerMock; + /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $loggerMock; + protected function setUp() { $objectManager = new ObjectManager($this); $this->serializerMock = $this->createMock(Json::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); $contextMock = $this->createMock(Context::class); $eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $contextMock->method('getEventDispatcher') ->willReturn($eventManagerMock); + $contextMock->method('getLogger') + ->willReturn($this->loggerMock); $this->serializedConfig = $objectManager->getObject( Serialized::class, [ @@ -72,6 +79,20 @@ public function afterLoadDataProvider() ]; } + public function testAfterLoadWithException() + { + $value = '{"key":'; + $expected = false; + $this->serializedConfig->setValue($value); + $this->serializerMock->expects($this->once()) + ->method('unserialize') + ->willThrowException(new \Exception()); + $this->loggerMock->expects($this->once()) + ->method('critical'); + $this->serializedConfig->afterLoad(); + $this->assertEquals($expected, $this->serializedConfig->getValue()); + } + /** * @param string $expected * @param int|double|string|array|boolean|null $value diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php index bb772f51c0dac..a731be96af963 100644 --- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php @@ -355,22 +355,60 @@ public function testSaveToCheckScopeDataSet() $this->model->save(); } - public function testSetDataByPath() + /** + * @param string $path + * @param string $value + * @param string $section + * @param array $groups + * @return void + * @dataProvider setDataByPathDataProvider + */ + public function testSetDataByPath(string $path, string $value, string $section, array $groups) { - $value = 'value'; - $path = '<section>/<group>/<field>'; $this->model->setDataByPath($path, $value); - $expected = [ - 'section' => '<section>', - 'groups' => [ - '<group>' => [ - 'fields' => [ - '<field>' => ['value' => $value], + $this->assertEquals($section, $this->model->getData('section')); + $this->assertEquals($groups, $this->model->getData('groups')); + } + + /** + * @return array + */ + public function setDataByPathDataProvider(): array + { + return [ + 'depth 3' => [ + 'a/b/c', + 'value1', + 'a', + [ + 'b' => [ + 'fields' => [ + 'c' => ['value' => 'value1'], + ], + ], + ], + ], + 'depth 5' => [ + 'a/b/c/d/e', + 'value1', + 'a', + [ + 'b' => [ + 'groups' => [ + 'c' => [ + 'groups' => [ + 'd' => [ + 'fields' => [ + 'e' => ['value' => 'value1'], + ], + ], + ], + ], + ], ], ], ], ]; - $this->assertSame($expected, $this->model->getData()); } /** @@ -384,14 +422,14 @@ public function testSetDataByPathEmpty() /** * @param string $path - * @param string $expectedException - * + * @return void * @dataProvider setDataByPathWrongDepthDataProvider */ - public function testSetDataByPathWrongDepth($path, $expectedException) + public function testSetDataByPathWrongDepth(string $path) { - $expectedException = 'Allowed depth of configuration is 3 (<section>/<group>/<field>). ' . $expectedException; - $this->expectException('\UnexpectedValueException'); + $currentDepth = count(explode('/', $path)); + $expectedException = 'Minimal depth of configuration is 3. Your configuration depth is ' . $currentDepth; + $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage($expectedException); $value = 'value'; $this->model->setDataByPath($path, $value); @@ -400,13 +438,11 @@ public function testSetDataByPathWrongDepth($path, $expectedException) /** * @return array */ - public function setDataByPathWrongDepthDataProvider() + public function setDataByPathWrongDepthDataProvider(): array { return [ - 'depth 2' => ['section/group', "Your configuration depth is 2 for path 'section/group'"], - 'depth 1' => ['section', "Your configuration depth is 1 for path 'section'"], - 'depth 4' => ['section/group/field/sub-field', "Your configuration depth is 4 for path" - . " 'section/group/field/sub-field'", ], + 'depth 2' => ['section/group'], + 'depth 1' => ['section'], ]; } } diff --git a/app/code/Magento/Config/composer.json b/app/code/Magento/Config/composer.json index 793d423280414..f36c29d387c9b 100644 --- a/app/code/Magento/Config/composer.json +++ b/app/code/Magento/Config/composer.json @@ -13,7 +13,7 @@ "magento/module-deploy": "100.2.*" }, "type": "magento2-module", - "version": "101.0.7", + "version": "101.0.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ConfigurableImportExport/composer.json b/app/code/Magento/ConfigurableImportExport/composer.json index b3d8af9f419d2..cf0e0e819a89c 100644 --- a/app/code/Magento/ConfigurableImportExport/composer.json +++ b/app/code/Magento/ConfigurableImportExport/composer.json @@ -12,7 +12,7 @@ "magento/module-store": "100.2.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index a80a15b59c2ce..efade5cd2c605 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -15,6 +15,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Configurable product view type. + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api @@ -277,6 +279,8 @@ protected function getOptionImages() } /** + * Collect price options. + * * @return array */ protected function getOptionPrices() @@ -315,6 +319,11 @@ protected function getOptionPrices() ), ], 'tierPrices' => $tierPrices, + 'msrpPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $product->getMsrp() + ), + ], ]; } return $prices; diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php index 6f5f106a8bb24..45057a3591044 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php @@ -50,7 +50,11 @@ public function __construct( */ public function execute() { - $this->getResponse()->representJson($this->jsonHelper->jsonEncode($this->saveAttributeOptions())); + $result = []; + if ($this->getRequest()->isPost()) { + $result = $this->saveAttributeOptions(); + } + $this->getResponse()->representJson($this->jsonHelper->jsonEncode($result)); } /** diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php index 01981b5dae9db..fcbd0075b4cd0 100644 --- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php +++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,6 +9,11 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; +/** + * Configurable product link management. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementInterface { /** @@ -67,7 +71,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getChildren($sku) { @@ -106,11 +110,15 @@ public function getChildren($sku) } /** - * {@inheritdoc} + * @inheritdoc + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + * @throws \Magento\Framework\Exception\CouldNotSaveException */ public function addChild($sku, $childSku) { - $product = $this->productRepository->get($sku); + $product = $this->productRepository->get($sku, true); $child = $this->productRepository->get($childSku); $childrenIds = array_values($this->configurableType->getChildrenIds($product->getId())[0]); @@ -144,7 +152,11 @@ public function addChild($sku, $childSku) } /** - * {@inheritdoc} + * @inheritdoc + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + * @throws \Magento\Framework\Exception\CouldNotSaveException */ public function removeChild($sku, $childSku) { diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index f98075f2294cc..46f10608bc95e 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -24,6 +24,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -1385,7 +1386,7 @@ function ($item) { */ private function getUsedProductsCacheKey($keyParts) { - return md5(implode('_', $keyParts)); + return sha1(implode('_', $keyParts)); } /** diff --git a/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php b/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php index 113839e57791d..ca100cbf85bfc 100644 --- a/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php +++ b/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php @@ -118,8 +118,10 @@ private function updateRelatedProductTypes(string $attributeId, array $relatedPr */ private function upgradeQuoteItemPrice(ModuleDataSetupInterface $setup) { - $connection = $setup->getConnection(); - $quoteItemTable = $setup->getTable('quote_item'); + $connectionName = 'checkout'; + $connection = $setup->getConnection($connectionName); + $quoteItemTable = $setup->getTable('quote_item', $connectionName); + $select = $connection->select(); $select->joinLeft( ['qi2' => $quoteItemTable], @@ -130,10 +132,10 @@ private function upgradeQuoteItemPrice(ModuleDataSetupInterface $setup) . ' AND qi1.parent_item_id IS NOT NULL' . ' AND qi2.product_type = "' . Configurable::TYPE_CODE . '"' ); - $updateQuoteItem = $setup->getConnection()->updateFromSelect( + $updateQuoteItem = $connection->updateFromSelect( $select, ['qi1' => $quoteItemTable] ); - $setup->getConnection()->query($updateQuoteItem); + $connection->query($updateQuoteItem); } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml index 161441736f940..cdb147e99ad58 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml @@ -82,4 +82,57 @@ <requiredEntity createDataKey="getConfigAttributeOption2"/> </createData> </actionGroup> + + <!--Create the configurable product with three child products--> + <actionGroup name="AdminCreateApiConfigurableProductWithThreeChildActionGroup" extends="AdminCreateApiConfigurableProductActionGroup"> + <remove keyForRemoval="createConfigProductOption"/> + <remove keyForRemoval="createConfigChildProduct1"/> + <remove keyForRemoval="createConfigChildProduct2"/> + <remove keyForRemoval="createConfigProductAddChild1"/> + <remove keyForRemoval="createConfigProductAddChild2"/> + + <createData entity="ProductAttributeOption3" after="createConfigProductAttributeOption2" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="3" after="getConfigAttributeOption2" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <createData entity="ApiSimpleOne" stepKey="createChildProduct1"> + <field key="price">50</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleOne" stepKey="createChildProduct2"> + <field key="price">60</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ApiSimpleOne" stepKey="createChildProduct3"> + <field key="price">70</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOptions"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <createData entity="ConfigurableProductAddChild" stepKey="createProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createChildProduct3"/> + </createData> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml index 36615d3af6b7b..bddd71f565c33 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontConfigurableProductWithFileCustomOptionTest"> <annotations> <features value="ConfigurableProduct"/> @@ -42,7 +42,9 @@ <argument name="category" value="$$createCategory$$"/> </actionGroup> <!--Add custom option to configurable product--> - <actionGroup ref="AddProductCustomOptionFile" stepKey="addCustomOptionToProduct"/> + <actionGroup ref="AddProductCustomOptionFile" stepKey="addCustomOptionToProduct"> + <argument name="option" value="ProductOptionFile"/> + </actionGroup> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> <!--Go to storefront--> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index b45306d670bff..20b0905b7707b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -347,15 +347,15 @@ public function testGetJsonConfig() } /** - * Retrieve array with expected parameters for method getJsonConfig() + * Retrieve array with expected parameters for method getJsonConfig(). * - * @param $productId - * @param $amount - * @param $priceQty - * @param $percentage + * @param int $productId + * @param float $amount + * @param int $priceQty + * @param int $percentage * @return array */ - private function getExpectedArray($productId, $amount, $priceQty, $percentage) + private function getExpectedArray(int $productId, float $amount, int $priceQty, int $percentage): array { $expectedArray = [ 'attributes' => [], @@ -379,6 +379,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage) 'percentage' => $percentage, ], ], + 'msrpPrice' => [ + 'amount' => null, + ], ], ], 'priceFormat' => [], diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php index 9fd225e8acaab..44d0ca86d98d0 100644 --- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php +++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php @@ -5,14 +5,14 @@ */ namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier; +use Magento\Catalog\Model\Locator\LocatorInterface; use Magento\Catalog\Model\Product\Attribute\Backend\Sku; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; +use Magento\Framework\UrlInterface; use Magento\Ui\Component\Container; -use Magento\Ui\Component\Form; use Magento\Ui\Component\DynamicRows; +use Magento\Ui\Component\Form; use Magento\Ui\Component\Modal; -use Magento\Framework\UrlInterface; -use Magento\Catalog\Model\Locator\LocatorInterface; /** * Data provider for Configurable panel @@ -90,7 +90,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -98,7 +98,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -197,7 +197,7 @@ public function modifyMeta(array $meta) 'autoRender' => false, 'componentType' => 'insertListing', 'component' => 'Magento_ConfigurableProduct/js' - .'/components/associated-product-insert-listing', + . '/components/associated-product-insert-listing', 'dataScope' => $this->associatedListingPrefix . static::ASSOCIATED_PRODUCT_LISTING, 'externalProvider' => $this->associatedListingPrefix @@ -328,14 +328,12 @@ protected function getButtonSet() 'component' => 'Magento_Ui/js/form/components/button', 'actions' => [ [ - 'targetName' => - $this->dataScopeName . '.configurableModal', + 'targetName' => $this->dataScopeName . '.configurableModal', 'actionName' => 'trigger', 'params' => ['active', true], ], [ - 'targetName' => - $this->dataScopeName . '.configurableModal', + 'targetName' => $this->dataScopeName . '.configurableModal', 'actionName' => 'openModal', ], ], @@ -574,6 +572,7 @@ protected function getColumn( 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => $name, 'visibleIfCanEdit' => false, + 'labelVisible' => false, 'imports' => [ 'visible' => '!${$.provider}:${$.parentScope}.canEdit' ], @@ -591,7 +590,9 @@ protected function getColumn( 'formElement' => Container::NAME, 'component' => 'Magento_Ui/js/form/components/group', 'label' => $label, + 'showLabel' => false, 'dataScope' => '', + 'showLabel' => false ]; $container['children'] = [ $name . '_edit' => $fieldEdit, diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json index c33dc148cddb1..90b1ce26a920b 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -26,7 +26,7 @@ "magento/module-tax": "100.2.*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index a390cb375befc..dd39bcb477699 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -248,4 +248,11 @@ <type name="Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector"> <plugin name="apply_tax_class_id" type="Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector" /> </type> + <type name="Magento\Eav\Model\Entity\Attribute\Group"> + <arguments> + <argument name="reservedSystemNames" xsi:type="array"> + <item name="configurable" xsi:type="string">configurable</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index 190ecccbfdb76..78fa8b1c68b7a 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -17,12 +17,11 @@ <legend class="legend admin__legend"> <span><?= /* @escapeNotVerified */ __('Associated Products') ?></span> </legend> - <div class="product-options fieldset admin__fieldset"> - <?php foreach ($_attributes as $_attribute): ?> - <div class="field admin__field _required required"> - <label class="label admin__field-label"><?php - /* @escapeNotVerified */ echo $_attribute->getProductAttribute() - ->getStoreLabel($_product->getStoreId()); + <div class="product-options"> + <div class="field admin__field _required required"> + <?php foreach ($_attributes as $_attribute): ?> + <label class="label admin__field-label"><?= + $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel($_product->getStoreId())) ?></label> <div class="control admin__field-control <?php if ($_attribute->getDecoratedIsLast()): @@ -34,8 +33,8 @@ <option><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> </select> </div> - </div> - <?php endforeach; ?> + <?php endforeach; ?> + </div> </div> </fieldset> <script> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js index 01abce7696014..df800c9a64a39 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js @@ -6,8 +6,9 @@ define([ 'underscore', 'uiRegistry', - 'Magento_Ui/js/dynamic-rows/dynamic-rows' -], function (_, registry, dynamicRows) { + 'Magento_Ui/js/dynamic-rows/dynamic-rows', + 'jquery' +], function (_, registry, dynamicRows, $) { 'use strict'; return dynamicRows.extend({ @@ -217,6 +218,8 @@ define([ _.each(tmpData, function (row, index) { path = this.dataScope + '.' + this.index + '.' + (this.startIndex + index); + row.attributes = $('<i></i>').text(row.attributes).html(); + row.sku = $('<i></i>').text(row.sku).html(); this.source.set(path, row); }, this); @@ -376,8 +379,8 @@ define([ product = { 'id': row.productId, 'product_link': row.productUrl, - 'name': row.name, - 'sku': row.sku, + 'name': $('<i></i>').text(row.name).html(), + 'sku': $('<i></i>').text(row.sku).html(), 'status': row.status, 'price': row.price, 'price_currency': row.priceCurrency, diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js index 6bbab77a3a0ab..b2ef35546eea8 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js @@ -11,9 +11,6 @@ define([ return Abstract.extend({ defaults: { - listens: { - isConfigurable: 'handlePriceValue' - }, imports: { isConfigurable: '!ns = ${ $.ns }, index = configurable-matrix:isEmpty' }, @@ -22,12 +19,15 @@ define([ } }, - /** - * Invokes initialize method of parent class, - * contains initialization logic - */ + /** @inheritdoc */ initialize: function () { this._super(); + // resolve initial disable state + this.handlePriceValue(this.isConfigurable); + // add listener to track "configurable" type + this.setListeners({ + isConfigurable: 'handlePriceValue' + }); return this; }, @@ -50,11 +50,10 @@ define([ * @param {String} isConfigurable */ handlePriceValue: function (isConfigurable) { + this.disabled(!!this.isUseDefault() || isConfigurable); + if (isConfigurable) { - this.disable(); this.clear(); - } else { - this.enable(); } } }); diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index 1df84d27a5c30..ef40dcb9a7323 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -373,7 +373,7 @@ define([ allowedProducts, i, j, - basePrice = parseFloat(this.options.spConfig.prices.basePrice.amount), + finalPrice = parseFloat(this.options.spConfig.prices.finalPrice.amount), optionFinalPrice, optionPriceDiff, optionPrices = this.options.spConfig.optionPrices, @@ -410,7 +410,7 @@ define([ typeof optionPrices[allowedProducts[0]] !== 'undefined') { allowedProductMinPrice = this._getAllowedProductWithMinPrice(allowedProducts); optionFinalPrice = parseFloat(optionPrices[allowedProductMinPrice].finalPrice.amount); - optionPriceDiff = optionFinalPrice - basePrice; + optionPriceDiff = optionFinalPrice - finalPrice; if (optionPriceDiff !== 0) { options[i].label = options[i].label + ' ' + priceUtils.formatPrice( @@ -609,6 +609,13 @@ define([ } else { $(this.options.slyOldPriceSelector).hide(); } + + $(document).trigger('updateMsrpPriceBlock', + [ + optionId, + this.options.spConfig.optionPrices + ] + ); }, /** diff --git a/app/code/Magento/Contact/composer.json b/app/code/Magento/Contact/composer.json index 72c8005c53432..bb95e20c1172a 100644 --- a/app/code/Magento/Contact/composer.json +++ b/app/code/Magento/Contact/composer.json @@ -10,7 +10,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Contact/view/frontend/web/css/source/_module.less b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less index 0aaec05aa2afe..d79806eecbe9b 100644 --- a/app/code/Magento/Contact/view/frontend/web/css/source/_module.less +++ b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less @@ -21,6 +21,16 @@ } } +// +// Desktop +// _____________________________________________ + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { + .contact-index-index .column:not(.sidebar-additional) .form.contact { + min-width: 600px; + } +} + // Mobile .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .contact-index-index { diff --git a/app/code/Magento/Cron/composer.json b/app/code/Magento/Cron/composer.json index ddbe7df2c0a2e..a1f1e107a3c57 100644 --- a/app/code/Magento/Cron/composer.json +++ b/app/code/Magento/Cron/composer.json @@ -10,7 +10,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php index ae13c4d399e47..3ab1bfc086721 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php @@ -7,15 +7,22 @@ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; +use Magento\Framework\Exception\NotFoundException; + class SaveRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency { /** * Save rates action * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $data = $this->getRequest()->getParam('rate'); if (is_array($data)) { try { diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php index eee7961b02f4a..ad80833d8da5d 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php @@ -6,15 +6,22 @@ */ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol; +use Magento\Framework\Exception\NotFoundException; + class Save extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol { /** * Save custom Currency symbol * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $symbolsDataArray = $this->getRequest()->getParam('custom_currency_symbol', null); if (is_array($symbolsDataArray)) { foreach ($symbolsDataArray as &$symbolsData) { @@ -27,9 +34,9 @@ public function execute() try { $this->_objectManager->create(\Magento\CurrencySymbol\Model\System\Currencysymbol::class) ->setCurrencySymbolsData($symbolsDataArray); - $this->messageManager->addSuccess(__('You applied the custom currency symbols.')); + $this->messageManager->addSuccessMessage(__('You applied the custom currency symbols.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*'))); diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php index 0863104a2bf8d..455449a449103 100644 --- a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php +++ b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php @@ -57,11 +57,18 @@ class SaveTest extends \PHPUnit\Framework\TestCase */ protected $filterManagerMock; + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); - $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->helperMock = $this->createMock(\Magento\Backend\Helper\Data::class); @@ -128,7 +135,7 @@ public function testExecute() ->willReturn($this->filterManagerMock); $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You applied the custom currency symbols.')); $this->action->execute(); diff --git a/app/code/Magento/CurrencySymbol/composer.json b/app/code/Magento/CurrencySymbol/composer.json index 021fe7ae9bc56..378b0df398ef5 100644 --- a/app/code/Magento/CurrencySymbol/composer.json +++ b/app/code/Magento/CurrencySymbol/composer.json @@ -11,7 +11,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit.php b/app/code/Magento/Customer/Block/Adminhtml/Edit.php index 973016baba29c..701e38bea6b58 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit.php @@ -122,7 +122,7 @@ protected function _construct() [ 'label' => __('Force Sign-In'), 'onclick' => 'deleteConfirm(\'' . $this->escapeJs($this->escapeHtml($deleteConfirmMsg)) . - '\', \'' . $url . '\')', + '\', \'' . $url . '\', {data: {}})', 'class' => 'invalidate-token' ], 10 diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php index 180cb3d66ea35..506ba3fb9bfda 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php @@ -25,7 +25,8 @@ public function getButtonData() $data = [ 'label' => __('Force Sign-In'), 'class' => 'invalidate-token', - 'on_click' => 'deleteConfirm("' . $deleteConfirmMsg . '", "' . $this->getInvalidateTokenUrl() . '")', + 'on_click' => 'deleteConfirm("' . $deleteConfirmMsg . '", "' . $this->getInvalidateTokenUrl() . + '", {data: {}})', 'sort_order' => 65, ]; } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php index bb190260e4776..c1266febff99d 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php @@ -63,7 +63,8 @@ protected function _construct() { parent::_construct(); $this->setId('customer_orders_grid'); - $this->setDefaultSort('created_at', 'desc'); + $this->setDefaultSort('created_at'); + $this->setDefaultDir('desc'); $this->setUseAjax(true); } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php index d0973d3baf383..988a157805b36 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php @@ -77,7 +77,8 @@ protected function _construct() { parent::_construct(); $this->setId('customer_view_cart_grid'); - $this->setDefaultSort('added_at', 'desc'); + $this->setDefaultSort('added_at'); + $this->setDefaultDir('desc'); $this->setSortable(false); $this->setPagerVisibility(false); $this->setFilterVisibility(false); diff --git a/app/code/Magento/Customer/Controller/Address/Delete.php b/app/code/Magento/Customer/Controller/Address/Delete.php index ef92bd2ef533b..d287808b4056d 100644 --- a/app/code/Magento/Customer/Controller/Address/Delete.php +++ b/app/code/Magento/Customer/Controller/Address/Delete.php @@ -6,13 +6,20 @@ */ namespace Magento\Customer\Controller\Address; +use Magento\Framework\Exception\NotFoundException; + class Delete extends \Magento\Customer\Controller\Address { /** * @return \Magento\Framework\Controller\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $addressId = $this->getRequest()->getParam('id', false); if ($addressId && $this->_formKeyValidator->validate($this->getRequest())) { @@ -20,12 +27,12 @@ public function execute() $address = $this->_addressRepository->getById($addressId); if ($address->getCustomerId() === $this->_getSession()->getCustomerId()) { $this->_addressRepository->deleteById($addressId); - $this->messageManager->addSuccess(__('You deleted the address.')); + $this->messageManager->addSuccessMessage(__('You deleted the address.')); } else { - $this->messageManager->addError(__('We can\'t delete the address right now.')); + $this->messageManager->addErrorMessage(__('We can\'t delete the address right now.')); } } catch (\Exception $other) { - $this->messageManager->addException($other, __('We can\'t delete the address right now.')); + $this->messageManager->addExceptionMessage($other, __('We can\'t delete the address right now.')); } } return $this->resultRedirectFactory->create()->setPath('*/*/index'); diff --git a/app/code/Magento/Customer/Controller/Address/Index.php b/app/code/Magento/Customer/Controller/Address/Index.php index ad04c7bd5c71b..674d3bcf0e0d3 100644 --- a/app/code/Magento/Customer/Controller/Address/Index.php +++ b/app/code/Magento/Customer/Controller/Address/Index.php @@ -28,9 +28,9 @@ class Index extends \Magento\Customer\Controller\Address * @param \Magento\Customer\Api\Data\RegionInterfaceFactory $regionDataFactory * @param \Magento\Framework\Reflection\DataObjectProcessor $dataProcessor * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper - * @param CustomerRepositoryInterface $customerRepository * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param CustomerRepositoryInterface $customerRepository * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php index 936d9cdbc1704..6b35397d9be13 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php @@ -9,6 +9,7 @@ use Magento\Customer\Api\Data\GroupInterfaceFactory; use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Exception\NotFoundException; class Save extends \Magento\Customer\Controller\Adminhtml\Group { @@ -66,9 +67,14 @@ protected function storeCustomerGroupDataToSession($customerGroupData) * Create or save customer group. * * @return \Magento\Backend\Model\View\Result\Redirect|\Magento\Backend\Model\View\Result\Forward + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $taxClass = (int)$this->getRequest()->getParam('tax_class'); /** @var \Magento\Customer\Api\Data\GroupInterface $customerGroup */ diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php index 1b5160ef31185..4d1bc18a98a06 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php @@ -116,8 +116,6 @@ private function getEmailNotification() * Inline edit action execute * * @return \Magento\Framework\Controller\Result\Json - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function execute() { @@ -125,7 +123,7 @@ public function execute() $resultJson = $this->resultJsonFactory->create(); $postItems = $this->getRequest()->getParam('items', []); - if (!($this->getRequest()->getParam('isAjax') && count($postItems))) { + if (!($this->getRequest()->getParam('isAjax') && $this->getRequest()->isPost() && count($postItems))) { return $resultJson->setData([ 'messages' => [__('Please correct the data sent.')], 'error' => true, diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php index 37c8ed5a252f8..eab18520e69a7 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php @@ -31,7 +31,9 @@ public function execute() \Magento\Customer\Model\AccountManagement::EMAIL_REMINDER, $customer->getWebsiteId() ); - $this->messageManager->addSuccess(__('The customer will receive an email with a link to reset password.')); + $this->messageManager->addSuccessMessage( + __('The customer will receive an email with a link to reset password.') + ); } catch (NoSuchEntityException $exception) { $resultRedirect->setPath('customer/index'); return $resultRedirect; @@ -44,7 +46,7 @@ public function execute() } catch (SecurityViolationException $exception) { $this->messageManager->addErrorMessage($exception->getMessage()); } catch (\Exception $exception) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $exception, __('Something went wrong while resetting customer password.') ); diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index e837517762473..379b4979e2070 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -19,6 +19,7 @@ use Magento\Customer\Model\Data\Customer; use Magento\Customer\Model\Metadata\Validator; use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; +use Magento\Directory\Model\AllowedCountries; use Magento\Eav\Model\Validator\Attribute\Backend; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -339,6 +340,11 @@ class AccountManagement implements AccountManagementInterface */ private $addressRegistry; + /** + * @var AllowedCountries + */ + private $allowedCountriesReader; + /** * @param CustomerFactory $customerFactory * @param ManagerInterface $eventManager @@ -371,8 +377,10 @@ class AccountManagement implements AccountManagementInterface * @param CollectionFactory|null $visitorCollectionFactory * @param SearchCriteriaBuilder|null $searchCriteriaBuilder * @param AddressRegistry|null $addressRegistry + * @param AllowedCountries|null $allowedCountriesReader * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function __construct( CustomerFactory $customerFactory, @@ -405,7 +413,8 @@ public function __construct( SaveHandlerInterface $saveHandler = null, CollectionFactory $visitorCollectionFactory = null, SearchCriteriaBuilder $searchCriteriaBuilder = null, - AddressRegistry $addressRegistry = null + AddressRegistry $addressRegistry = null, + AllowedCountries $allowedCountriesReader = null ) { $this->customerFactory = $customerFactory; $this->eventManager = $eventManager; @@ -445,6 +454,8 @@ public function __construct( ?: ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); + $this->allowedCountriesReader = $allowedCountriesReader + ?: ObjectManager::getInstance()->get(AllowedCountries::class); } /** @@ -882,6 +893,9 @@ public function createAccountWithPasswordHash( } try { foreach ($customerAddresses as $address) { + if (!$this->isAddressAllowedForWebsite($address, $customer->getStoreId())) { + continue; + } if ($address->getId()) { $newAddress = clone $address; $newAddress->setId(null); @@ -1572,4 +1586,18 @@ private function setIgnoreValidationFlag(Customer $customer) { $customer->setData('ignore_validation_flag', true); } + + /** + * Check is address allowed for store + * + * @param AddressInterface $address + * @param int|null $storeId + * @return bool + */ + private function isAddressAllowedForWebsite(AddressInterface $address, $storeId): bool + { + $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_STORE, $storeId); + + return in_array($address->getCountryId(), $allowedCountries); + } } diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index ce976d3f62c74..9c9f04185477e 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -375,45 +375,17 @@ protected function getAttributesMeta(Type $entityType) return $meta; } - /** - * Check whether the specific attribute can be shown in form: customer registration, customer edit, etc... - * - * @param Attribute $customerAttribute - * @return bool - */ - private function canShowAttributeInForm(AbstractAttribute $customerAttribute) - { - $isRegistration = $this->context->getRequestParam($this->getRequestFieldName()) === null; - - if ($customerAttribute->getEntityType()->getEntityTypeCode() === 'customer') { - return is_array($customerAttribute->getUsedInForms()) && - ( - (in_array('customer_account_create', $customerAttribute->getUsedInForms()) && $isRegistration) || - (in_array('customer_account_edit', $customerAttribute->getUsedInForms()) && !$isRegistration) - ); - } else { - return is_array($customerAttribute->getUsedInForms()) && - in_array('customer_address_edit', $customerAttribute->getUsedInForms()); - } - } - /** * Detect can we show attribute on specific form or not * * @param Attribute $customerAttribute * @return bool */ - private function canShowAttribute(AbstractAttribute $customerAttribute) + private function canShowAttribute(AbstractAttribute $customerAttribute): bool { - $userDefined = (bool) $customerAttribute->getIsUserDefined(); - if (!$userDefined) { - return $customerAttribute->getIsVisible(); - } - - $canShowOnForm = $this->canShowAttributeInForm($customerAttribute); - - return ($this->allowToShowHiddenAttributes && $canShowOnForm) || - (!$this->allowToShowHiddenAttributes && $canShowOnForm && $customerAttribute->getIsVisible()); + return $this->allowToShowHiddenAttributes && (bool) $customerAttribute->getIsUserDefined() + ? true + : (bool) $customerAttribute->getIsVisible(); } /** diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerActionGroup.xml new file mode 100644 index 0000000000000..b9bac35b503b3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSaveCustomerForm"> + <scrollToTopOfPage stepKey="scrollToPageTop"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the customer." stepKey="seeSaveMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml index 50a238323e331..aa764e5f51de1 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml @@ -17,4 +17,10 @@ <waitForPageLoad stepKey="waitForPageLoad"/> <see selector="{{StorefrontHeaderSection.mainTitle}}" userInput="{{tabName}}" stepKey="checkTabTitle"/> </actionGroup> + + <!--Go to Storefront > Account Information--> + <actionGroup name="StorefrontStartCustomerAccountInformationEdit"> + <amOnPage url="{{StorefrontCustomerAccountInformationPage.url}}" stepKey="goToAccountInformationEditPage"/> + <see selector="{{StorefrontCustomerAccountInformationSection.title}}" userInput="Edit Account Information" stepKey="seeEditAccountInformationPageTitle"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountInformationPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountInformationPage.xml new file mode 100644 index 0000000000000..80caea5a1f541 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountInformationPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerAccountInformationPage" url="/customer/account/edit" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerSidebarSection"/> + <section name="StorefrontCustomerAccountInformationSection" /> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressNewPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressNewPage.xml new file mode 100644 index 0000000000000..8fbc7b10fdd95 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressNewPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerAddressNewPage" url="/customer/address/new" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerAddressEditFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml new file mode 100644 index 0000000000000..f6706d0e16ab3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAccountInformationSection"> + <element name="title" type="text" selector=".page-title span"/> + <element name="firstName" type="input" selector="#firstname"/> + <element name="lastName" type="input" selector="#lastname"/> + <element name="changeEmail" type="checkbox" selector="#change_email"/> + <element name="changePassword" type="checkbox" selector="#change_password"/> + <element name="customAttributeFiled" type="input" selector="#{{attribute_code}}" parameterized="true"/> + <element name="saveButton" type="button" selector="#form-validate .action.save.primary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressEditFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressEditFormSection.xml new file mode 100644 index 0000000000000..2af00532301ed --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressEditFormSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAddressEditFormSection"> + <element name="country" type="select" selector=".form-address-edit select#country" /> + <element name="countryEmptyOption" type="select" selector=".form-address-edit select#country option[value='']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php index 4064b8586257d..f28053a6611fc 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/DeleteTest.php @@ -78,7 +78,9 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['isPost']) ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->address = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) ->getMockForAbstractClass(); $this->messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) @@ -146,7 +148,7 @@ public function testExecute() ->method('deleteById') ->with($addressId); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You deleted the address.')); $this->resultRedirect->expects($this->once()) ->method('setPath') @@ -183,11 +185,11 @@ public function testExecuteWithException() ->willReturn(34); $exception = new \Exception('Exception'); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('We can\'t delete the address right now.')) ->willThrowException($exception); $this->messageManager->expects($this->once()) - ->method('addException') + ->method('addExceptionMessage') ->with($exception, __('We can\'t delete the address right now.')); $this->resultRedirect->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php index 5f7064d5b124b..55967854f97ca 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Group/SaveTest.php @@ -88,7 +88,9 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['isPost']) ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->resultRedirectFactory = $this->getMockBuilder(RedirectFactory::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php index fe7868ef3eb3f..1e10d702174c3 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php @@ -87,12 +87,10 @@ protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->request = $this->getMockForAbstractClass( - \Magento\Framework\App\RequestInterface::class, - [], - '', - false - ); + $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->messageManager = $this->getMockForAbstractClass( \Magento\Framework\Message\ManagerInterface::class, [], diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php index 02d071ab394a5..35ac522499b63 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ResetPasswordTest.php @@ -143,7 +143,7 @@ protected function setUp() $this->messageManager = $this->getMockBuilder( \Magento\Framework\Message\Manager::class )->disableOriginalConstructor()->setMethods( - ['addSuccess', 'addMessage', 'addException', 'addErrorMessage'] + ['addSuccessMessage', 'addMessage', 'addExceptionMessage', 'addErrorMessage'] )->getMock(); $this->resultRedirectFactoryMock = $this->getMockBuilder( @@ -443,7 +443,7 @@ public function testResetPasswordActionException() $this->messageManager->expects( $this->once() )->method( - 'addException' + 'addExceptionMessage' )->with( $this->equalTo($exception), $this->equalTo('Something went wrong while resetting customer password.') @@ -503,7 +503,7 @@ public function testResetPasswordActionSendEmail() $this->messageManager->expects( $this->once() )->method( - 'addSuccess' + 'addSuccessMessage' )->with( $this->equalTo('The customer will receive an email with a link to reset password.') ); diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index 50c21379054bf..f6a3ecb810aa5 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -1295,16 +1295,15 @@ public function testGetDataWithVisibleAttributesWithAccountEdit() $meta = $dataProvider->getMeta(); $this->assertNotEmpty($meta); - $this->assertEquals($this->getExpectationForVisibleAttributes(false), $meta); + $this->assertEquals($this->getExpectationForVisibleAttributes(), $meta); } /** * Retrieve all customer variations of attributes with all variations of visibility * - * @param bool $isRegistration * @return array */ - private function getCustomerAttributeExpectations($isRegistration) + private function getCustomerAttributeExpectations() { return [ self::ATTRIBUTE_CODE . "_1" => [ @@ -1314,7 +1313,7 @@ private function getCustomerAttributeExpectations($isRegistration) 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', 'options' => 'test-options', - 'visible' => !$isRegistration, + 'visible' => true, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -1351,7 +1350,7 @@ private function getCustomerAttributeExpectations($isRegistration) 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'visible' => $isRegistration, + 'visible' => true, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -1374,7 +1373,7 @@ private function getCustomerAttributeExpectations($isRegistration) 'config' => [ 'dataType' => 'frontend_input', 'formElement' => 'frontend_input', - 'visible' => $isRegistration, + 'visible' => true, 'required' => 'is_required', 'label' => __('frontend_label'), 'sortOrder' => 'sort_order', @@ -1397,14 +1396,13 @@ private function getCustomerAttributeExpectations($isRegistration) /** * Retrieve all variations of attributes with all variations of visibility * - * @param bool $isRegistration * @return array */ - private function getExpectationForVisibleAttributes($isRegistration = true) + private function getExpectationForVisibleAttributes() { return [ 'customer' => [ - 'children' => $this->getCustomerAttributeExpectations($isRegistration), + 'children' => $this->getCustomerAttributeExpectations(), ], 'address' => [ 'children' => [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php index c655ff7056ed6..8421320dc7322 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php @@ -23,7 +23,7 @@ public function testRender($regionCollection) $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); $elementMock = $this->createPartialMock( \Magento\Framework\Data\Form\Element\AbstractElement::class, - ['getForm', 'getHtmlAttributes'] + ['getForm', 'getHtmlAttributes', 'getHtmlId', 'getName'] ); $countryMock = $this->createPartialMock( \Magento\Framework\Data\Form\Element\AbstractElement::class, diff --git a/app/code/Magento/Customer/composer.json b/app/code/Magento/Customer/composer.json index af45eb7931308..e8d5fe2aaff48 100644 --- a/app/code/Magento/Customer/composer.json +++ b/app/code/Magento/Customer/composer.json @@ -29,7 +29,7 @@ "magento/module-customer-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.7", + "version": "101.0.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Customer/view/frontend/web/js/address.js b/app/code/Magento/Customer/view/frontend/web/js/address.js index c6d05b51bdf0b..5499327c2e27d 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/address.js +++ b/app/code/Magento/Customer/view/frontend/web/js/address.js @@ -6,9 +6,10 @@ define([ 'jquery', 'Magento_Ui/js/modal/confirm', + 'mage/dataPost', 'jquery/ui', 'mage/translate' -], function ($, confirm) { +], function ($, confirm, dataPost) { 'use strict'; $.widget('mage.address', { @@ -53,7 +54,8 @@ define([ * @return {Boolean} */ _deleteAddress: function (e) { - var self = this; + var self = this, + addressId; confirm({ content: this.options.deleteConfirmMessage, @@ -62,12 +64,17 @@ define([ /** @inheritdoc */ confirm: function () { if (typeof $(e.target).parent().data('address') !== 'undefined') { - window.location = self.options.deleteUrlPrefix + $(e.target).parent().data('address') + - '/form_key/' + $.mage.cookies.get('form_key'); + addressId = $(e.target).parent().data('address'); } else { - window.location = self.options.deleteUrlPrefix + $(e.target).data('address') + - '/form_key/' + $.mage.cookies.get('form_key'); + addressId = $(e.target).data('address'); } + + dataPost().postData({ + action: self.options.deleteUrlPrefix + addressId, + data: { + 'form_key': $.mage.cookies.get('form_key') + } + }); } } }); diff --git a/app/code/Magento/CustomerImportExport/composer.json b/app/code/Magento/CustomerImportExport/composer.json index ebf8f7f52b99a..e0571ba1247ea 100644 --- a/app/code/Magento/CustomerImportExport/composer.json +++ b/app/code/Magento/CustomerImportExport/composer.json @@ -12,7 +12,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Deploy/Console/DeployStaticOptions.php b/app/code/Magento/Deploy/Console/DeployStaticOptions.php index 9a73dd5d65fc7..096d926262057 100644 --- a/app/code/Magento/Deploy/Console/DeployStaticOptions.php +++ b/app/code/Magento/Deploy/Console/DeployStaticOptions.php @@ -6,6 +6,7 @@ namespace Magento\Deploy\Console; +use Magento\Deploy\Process\Queue; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -57,6 +58,11 @@ class DeployStaticOptions */ const JOBS_AMOUNT = 'jobs'; + /** + * Key for max execution time option + */ + const MAX_EXECUTION_TIME = 'max-execution-time'; + /** * Force run of static deploy */ @@ -150,6 +156,7 @@ public function getOptionsList() * Basic options * * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ private function getBasicOptions() { @@ -216,6 +223,13 @@ private function getBasicOptions() 'Enable parallel processing using the specified number of jobs.', self::DEFAULT_JOBS_AMOUNT ), + new InputOption( + self::MAX_EXECUTION_TIME, + null, + InputOption::VALUE_OPTIONAL, + 'The maximum expected execution time of deployment static process (in seconds).', + Queue::DEFAULT_MAX_EXEC_TIME + ), new InputOption( self::SYMLINK_LOCALE, null, diff --git a/app/code/Magento/Deploy/Console/InputValidator.php b/app/code/Magento/Deploy/Console/InputValidator.php index b3301f60fec26..0e07f53ee500a 100644 --- a/app/code/Magento/Deploy/Console/InputValidator.php +++ b/app/code/Magento/Deploy/Console/InputValidator.php @@ -5,10 +5,11 @@ */ namespace Magento\Deploy\Console; -use Magento\Setup\Console\Command\DeployStaticContentCommand; +use Magento\Framework\App\ObjectManager; use Magento\Deploy\Console\DeployStaticOptions as Options; use Magento\Framework\Validator\Locale; use Symfony\Component\Console\Input\InputInterface; +use Magento\Framework\Validator\RegexFactory; /** * Command input arguments validator class @@ -55,14 +56,24 @@ class InputValidator */ private $localeValidator; + /** + * @var RegexFactory + */ + private $versionValidatorFactory; + /** * InputValidator constructor * * @param Locale $localeValidator + * @param RegexFactory|null $versionValidatorFactory */ - public function __construct(Locale $localeValidator) - { + public function __construct( + Locale $localeValidator, + RegexFactory $versionValidatorFactory = null + ) { $this->localeValidator = $localeValidator; + $this->versionValidatorFactory = $versionValidatorFactory ?: ObjectManager::getInstance() + ->get(RegexFactory::class); } /** @@ -70,6 +81,7 @@ public function __construct(Locale $localeValidator) * * @param InputInterface $input * @return void + * @throws \InvalidArgumentException */ public function validate(InputInterface $input) { @@ -85,6 +97,9 @@ public function validate(InputInterface $input) $input->getArgument(Options::LANGUAGES_ARGUMENT) ?: ['all'], $input->getOption(Options::EXCLUDE_LANGUAGE) ); + $this->checkVersionInput( + $input->getOption(Options::CONTENT_VERSION) ?: '' + ); } /** @@ -147,4 +162,22 @@ private function checkLanguagesInput(array $languagesInclude, array $languagesEx } } } + + /** + * @param string $contentVersion + * @throws \InvalidArgumentException + */ + private function checkVersionInput(string $contentVersion) + { + if ($contentVersion) { + $versionValidator = $this->versionValidatorFactory->create(['pattern' => '/^[A-Za-z0-9_.]+$/']); + if (!$versionValidator->isValid($contentVersion)) { + throw new \InvalidArgumentException(__( + 'Argument "' . + Options::CONTENT_VERSION + . '" has invalid value, content version should contain only characters, digits and dots' + )); + } + } + } } diff --git a/app/code/Magento/Deploy/Service/DeployStaticContent.php b/app/code/Magento/Deploy/Service/DeployStaticContent.php index 72de645868e22..aaef3f6e9ea5b 100644 --- a/app/code/Magento/Deploy/Service/DeployStaticContent.php +++ b/app/code/Magento/Deploy/Service/DeployStaticContent.php @@ -85,24 +85,26 @@ public function deploy(array $options) return; } - $queue = $this->queueFactory->create( - [ - 'logger' => $this->logger, - 'options' => $options, - 'maxProcesses' => $this->getProcessesAmount($options), - 'deployPackageService' => $this->objectManager->create( - \Magento\Deploy\Service\DeployPackage::class, - [ - 'logger' => $this->logger - ] - ) - ] - ); + $queueOptions = [ + 'logger' => $this->logger, + 'options' => $options, + 'maxProcesses' => $this->getProcessesAmount($options), + 'deployPackageService' => $this->objectManager->create( + \Magento\Deploy\Service\DeployPackage::class, + [ + 'logger' => $this->logger + ] + ) + ]; + + if (isset($options[Options::MAX_EXECUTION_TIME])) { + $queueOptions['maxExecTime'] = (int)$options[Options::MAX_EXECUTION_TIME]; + } $deployStrategy = $this->deployStrategyFactory->create( $options[Options::STRATEGY], [ - 'queue' => $queue + 'queue' => $this->queueFactory->create($queueOptions) ] ); @@ -134,6 +136,8 @@ public function deploy(array $options) } /** + * Returns amount of parallel processes, returns zero if option wasn't set. + * * @param array $options * @return int */ @@ -143,6 +147,8 @@ private function getProcessesAmount(array $options) } /** + * Checks if need to refresh only version. + * * @param array $options * @return bool */ diff --git a/app/code/Magento/Deploy/Test/Unit/Console/InputValidatorTest.php b/app/code/Magento/Deploy/Test/Unit/Console/InputValidatorTest.php new file mode 100644 index 0000000000000..2dc9eca7c5e14 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Console/InputValidatorTest.php @@ -0,0 +1,208 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Deploy\Test\Unit\Console; + +use Magento\Framework\Validator\Regex; +use Magento\Framework\Validator\RegexFactory; +use PHPUnit\Framework\TestCase; +use Magento\Deploy\Console\InputValidator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Deploy\Console\DeployStaticOptions as Options; +use Magento\Framework\Validator\Locale; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\ArrayInput; +use InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; + +/** + * Class InputValidatorTest + * @package Magento\Deploy\Test\Unit\Console + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class InputValidatorTest extends TestCase +{ + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * @var InputValidator + */ + protected $inputValidator; + + /** + * @var Locale + */ + protected $localeValidator; + + /** + * @throws \Zend_Validate_Exception + */ + protected function setUp() + { + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $regexFactoryMock = $this->getMockBuilder(RegexFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $regexObject = new Regex('/^[A-Za-z0-9_.]+$/'); + + $regexFactoryMock->expects($this->any())->method('create') + ->willReturn($regexObject); + + $localeObjectMock = $this->getMockBuilder(Locale::class)->setMethods(['isValid']) + ->disableOriginalConstructor() + ->getMock(); + + $localeObjectMock->expects($this->any())->method('isValid') + ->with('en_US') + ->will($this->returnValue(true)); + + $this->inputValidator = $this->objectManagerHelper->getObject( + InputValidator::class, + [ + 'localeValidator' => $localeObjectMock, + 'versionValidatorFactory' => $regexFactoryMock + ] + ); + } + + /** + * @throws \Zend_Validate_Exception + */ + public function testValidate() + { + $input = $this->getMockBuilder(ArrayInput::class) + ->disableOriginalConstructor() + ->setMethods(['getOption', 'getArgument']) + ->getMock(); + + $input->expects($this->atLeastOnce())->method('getArgument')->willReturn(['all']); + + $input->expects($this->atLeastOnce())->method('getOption') + ->willReturnMap( + [ + [Options::AREA, ['all']], + [Options::EXCLUDE_AREA, ['none']], + [Options::THEME, ['all']], + [Options::EXCLUDE_THEME, ['none']], + [Options::EXCLUDE_LANGUAGE, ['none']], + [Options::CONTENT_VERSION, '12345'] + ] + ); + + /** @noinspection PhpParamsInspection */ + $this->inputValidator->validate($input); + } + + /** + * @covers \Magento\Deploy\Console\InputValidator::checkAreasInput() + */ + public function testCheckAreasInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, null, ['test']), + new InputOption(Options::EXCLUDE_AREA, null, 4, null, ['test']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains('--area (-a) and --exclude-area cannot be used at the same time', $e->getMessage()); + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + /** + * @covers \Magento\Deploy\Console\InputValidator::checkThemesInput() + */ + public function testCheckThemesInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, null, ['all']), + new InputOption(Options::EXCLUDE_AREA, null, 4, null, ['none']), + new InputOption(Options::THEME, null, 4, '', ['blank']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['luma']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains('--theme (-t) and --exclude-theme cannot be used at the same time', $e->getMessage()); + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + public function testCheckLanguagesInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, null, ['all']), + new InputOption(Options::EXCLUDE_AREA, null, 4, null, ['none']), + new InputOption(Options::THEME, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['none']), + new InputArgument(Options::LANGUAGES_ARGUMENT, null, 4, ['en_US']), + new InputOption(Options::EXCLUDE_LANGUAGE, null, 4, '', ['all']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains( + '--language (-l) and --exclude-language cannot be used at the same time', + $e->getMessage() + ); + + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + public function testCheckVersionInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, null, ['all']), + new InputOption(Options::EXCLUDE_AREA, null, 4, null, ['none']), + new InputOption(Options::THEME, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['none']), + new InputArgument(Options::LANGUAGES_ARGUMENT, null, 4, ['en_US']), + new InputOption(Options::EXCLUDE_LANGUAGE, null, 4, '', ['none']), + new InputOption(Options::CONTENT_VERSION, null, 4, '', '/*!#') + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains( + 'Argument "' . + Options::CONTENT_VERSION + . '" has invalid value, content version should contain only characters, digits and dots', + $e->getMessage() + ); + + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php index 75edc8cb4f6ee..396381960e544 100644 --- a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Deploy\Test\Unit\Service; +use Magento\Deploy\Console\DeployStaticOptions; use Magento\Deploy\Package\Package; use Magento\Deploy\Process\Queue; use Magento\Deploy\Service\Bundle; @@ -221,4 +222,35 @@ public function deployDataProvider() ] ]; } + + public function testMaxExecutionTimeOptionPassed() + { + $options = [ + DeployStaticOptions::MAX_EXECUTION_TIME => 100, + DeployStaticOptions::REFRESH_CONTENT_VERSION_ONLY => false, + DeployStaticOptions::JOBS_AMOUNT => 3, + DeployStaticOptions::STRATEGY => 'compact', + DeployStaticOptions::NO_JAVASCRIPT => true, + DeployStaticOptions::NO_HTML_MINIFY => true, + ]; + + $queueMock = $this->createMock(Queue::class); + $strategyMock = $this->createMock(CompactDeploy::class); + $this->queueFactory->expects($this->once()) + ->method('create') + ->with([ + 'logger' => $this->logger, + 'maxExecTime' => 100, + 'maxProcesses' => 3, + 'options' => $options, + 'deployPackageService' => null + ]) + ->willReturn($queueMock); + $this->deployStrategyFactory->expects($this->once()) + ->method('create') + ->with('compact', ['queue' => $queueMock]) + ->willReturn($strategyMock); + + $this->service->deploy($options); + } } diff --git a/app/code/Magento/Deploy/composer.json b/app/code/Magento/Deploy/composer.json index 79ccab30d2924..b33313081b0f0 100644 --- a/app/code/Magento/Deploy/composer.json +++ b/app/code/Magento/Deploy/composer.json @@ -10,7 +10,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Developer/composer.json b/app/code/Magento/Developer/composer.json index 771853609211a..776d565bf5efa 100644 --- a/app/code/Magento/Developer/composer.json +++ b/app/code/Magento/Developer/composer.json @@ -8,7 +8,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 55df0748b1f06..6cb9f0e917dfb 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -7,6 +7,7 @@ namespace Magento\Dhl\Model; use Magento\Catalog\Model\Product\Type; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\Module\Dir; use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\Order\Shipment; @@ -56,6 +57,27 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin */ const CODE = 'dhl'; + /** + * DHL service Quote prefix used for message reference. + * + * @var string + */ + private $servicePrefixQuote = 'QUOT'; + + /** + * DHL service Shipping prefix used for message reference. + * + * @var string + */ + private $servicePrefixShipval = 'SHIP'; + + /** + * DHL service Tracking prefix used for message reference. + * + * @var string + */ + private $servicePrefixTracking = 'TRCK'; + /** * Rate request data * @@ -206,6 +228,11 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin */ private $xmlValidator; + /** + * @var ProductMetadataInterface + */ + private $productMetadata; + /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory @@ -232,7 +259,8 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory * @param array $data - * @param \Magento\Dhl\Model\Validator\XmlValidatorFactory $xmlValidatorFactory + * @param \Magento\Dhl\Model\Validator\XmlValidatorFactory|null $xmlValidatorFactory + * @param ProductMetadataInterface|null $productMetadata * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -261,7 +289,8 @@ public function __construct( \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory, array $data = [], - \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator = null + \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator = null, + ProductMetadataInterface $productMetadata = null ) { $this->readFactory = $readFactory; $this->_carrierHelper = $carrierHelper; @@ -295,6 +324,8 @@ public function __construct( } $this->xmlValidator = $xmlValidator ?: \Magento\Framework\App\ObjectManager::getInstance()->get(XmlValidator::class); + $this->productMetadata = $productMetadata + ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ProductMetadataInterface::class); } /** @@ -983,19 +1014,30 @@ protected function _getQuotesFromServer($request) protected function _buildQuotesRequestXml() { $rawRequest = $this->_rawRequest; - $xmlStr = '<?xml version = "1.0" encoding = "UTF-8"?>' . - '<p:DCTRequest xmlns:p="http://www.dhl.com" xmlns:p1="http://www.dhl.com/datatypes" ' . - 'xmlns:p2="http://www.dhl.com/DCTRequestdatatypes" ' . + + $xmlStr = '<?xml version="1.0" encoding="UTF-8"?>' . + '<req:DCTRequest schemaVersion="2.0" ' . + 'xmlns:req="http://www.dhl.com" ' . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' . - 'xsi:schemaLocation="http://www.dhl.com DCT-req.xsd "/>'; + 'xsi:schemaLocation="http://www.dhl.com DCT-req_global-2.0.xsd"/>'; + $xml = $this->_xmlElFactory->create(['data' => $xmlStr]); $nodeGetQuote = $xml->addChild('GetQuote', '', ''); $nodeRequest = $nodeGetQuote->addChild('Request'); $nodeServiceHeader = $nodeRequest->addChild('ServiceHeader'); + $nodeServiceHeader->addChild('MessageTime', $this->buildMessageTimestamp()); + $nodeServiceHeader->addChild( + 'MessageReference', + $this->buildMessageReference($this->servicePrefixQuote) + ); $nodeServiceHeader->addChild('SiteID', (string)$this->getConfigData('id')); $nodeServiceHeader->addChild('Password', (string)$this->getConfigData('password')); + $nodeMetaData = $nodeRequest->addChild('MetaData'); + $nodeMetaData->addChild('SoftwareName', $this->buildSoftwareName()); + $nodeMetaData->addChild('SoftwareVersion', $this->buildSoftwareVersion()); + $nodeFrom = $nodeGetQuote->addChild('From'); $nodeFrom->addChild('CountryCode', $rawRequest->getOrigCountryId()); $nodeFrom->addChild('Postalcode', $rawRequest->getOrigPostal()); @@ -1386,44 +1428,37 @@ protected function _doRequest() { $rawRequest = $this->_request; - $originRegion = $this->getCountryParams( - $this->_scopeConfig->getValue( - Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $this->getStore() - ) - )->getRegion(); - - if (!$originRegion) { - throw new \Magento\Framework\Exception\LocalizedException(__('Wrong Region')); - } - - if ($originRegion == 'AM') { - $originRegion = ''; - } - $xmlStr = '<?xml version="1.0" encoding="UTF-8"?>' . - '<req:ShipmentValidateRequest' . - $originRegion . + '<req:ShipmentRequest' . ' xmlns:req="http://www.dhl.com"' . ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' . - ' xsi:schemaLocation="http://www.dhl.com ship-val-req' . - ($originRegion ? '_' . - $originRegion : '') . - '.xsd" />'; + ' xsi:schemaLocation="http://www.dhl.com ship-val-global-req-6.0.xsd"' . + ' schemaVersion="6.0" />'; $xml = $this->_xmlElFactory->create(['data' => $xmlStr]); $nodeRequest = $xml->addChild('Request', '', ''); $nodeServiceHeader = $nodeRequest->addChild('ServiceHeader'); + $nodeServiceHeader->addChild('MessageTime', $this->buildMessageTimestamp()); + // MessageReference must be 28 to 32 chars. + $nodeServiceHeader->addChild( + 'MessageReference', + $this->buildMessageReference($this->servicePrefixShipval) + ); $nodeServiceHeader->addChild('SiteID', (string)$this->getConfigData('id')); $nodeServiceHeader->addChild('Password', (string)$this->getConfigData('password')); - if (!$originRegion) { - $xml->addChild('RequestedPickupTime', 'N', ''); - } - if ($originRegion !== 'AP') { - $xml->addChild('NewShipper', 'N', ''); + $originRegion = $this->getCountryParams( + $this->_scopeConfig->getValue( + Shipment::XML_PATH_STORE_COUNTRY_ID, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $this->getStore() + ) + )->getRegion(); + if ($originRegion) { + $xml->addChild('RegionCode', $originRegion, ''); } + $xml->addChild('RequestedPickupTime', 'N', ''); + $xml->addChild('NewShipper', 'N', ''); $xml->addChild('LanguageCode', 'EN', ''); $xml->addChild('PiecesEnabled', 'Y', ''); @@ -1465,8 +1500,9 @@ protected function _doRequest() } $nodeConsignee->addChild('City', $rawRequest->getRecipientAddressCity()); - if ($originRegion !== 'AP') { - $nodeConsignee->addChild('Division', $rawRequest->getRecipientAddressStateOrProvinceCode()); + $recipientAddressStateOrProvinceCode = $rawRequest->getRecipientAddressStateOrProvinceCode(); + if ($recipientAddressStateOrProvinceCode) { + $nodeConsignee->addChild('Division', $recipientAddressStateOrProvinceCode); } $nodeConsignee->addChild('PostalCode', $rawRequest->getRecipientAddressPostalCode()); $nodeConsignee->addChild('CountryCode', $rawRequest->getRecipientAddressCountryCode()); @@ -1510,15 +1546,13 @@ protected function _doRequest() $nodeReference->addChild('ReferenceType', 'St'); /** Shipment Details */ - $this->_shipmentDetails($xml, $rawRequest, $originRegion); + $this->_shipmentDetails($xml, $rawRequest); /** Shipper */ $nodeShipper = $xml->addChild('Shipper', '', ''); $nodeShipper->addChild('ShipperID', (string)$this->getConfigData('account')); $nodeShipper->addChild('CompanyName', $rawRequest->getShipperContactCompanyName()); - if ($originRegion !== 'AP') { - $nodeShipper->addChild('RegisteredAccount', (string)$this->getConfigData('account')); - } + $nodeShipper->addChild('RegisteredAccount', (string)$this->getConfigData('account')); $address = $rawRequest->getShipperAddressStreet1() . ' ' . $rawRequest->getShipperAddressStreet2(); $address = $this->string->split($address, 35, false, true); @@ -1531,8 +1565,9 @@ protected function _doRequest() } $nodeShipper->addChild('City', $rawRequest->getShipperAddressCity()); - if ($originRegion !== 'AP') { - $nodeShipper->addChild('Division', $rawRequest->getShipperAddressStateOrProvinceCode()); + $shipperAddressStateOrProvinceCode = $rawRequest->getShipperAddressStateOrProvinceCode(); + if ($shipperAddressStateOrProvinceCode) { + $nodeShipper->addChild('Division', $shipperAddressStateOrProvinceCode); } $nodeShipper->addChild('PostalCode', $rawRequest->getShipperAddressPostalCode()); $nodeShipper->addChild('CountryCode', $rawRequest->getShipperAddressCountryCode()); @@ -1584,19 +1619,13 @@ protected function _doRequest() * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') { $nodeShipmentDetails = $xml->addChild('ShipmentDetails', '', ''); $nodeShipmentDetails->addChild('NumberOfPieces', count($rawRequest->getPackages())); - if ($originRegion) { - $nodeShipmentDetails->addChild( - 'CurrencyCode', - $this->_storeManager->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() - ); - } - $nodePieces = $nodeShipmentDetails->addChild('Pieces', '', ''); /* @@ -1615,18 +1644,12 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') } $nodePiece->addChild('PieceID', ++$i); $nodePiece->addChild('PackageType', $packageType); - $nodePiece->addChild('Weight', sprintf('%.1f', $package['params']['weight'])); + $nodePiece->addChild('Weight', sprintf('%.3f', $package['params']['weight'])); $params = $package['params']; if ($params['width'] && $params['length'] && $params['height']) { - if (!$originRegion) { - $nodePiece->addChild('Width', round($params['width'])); - $nodePiece->addChild('Height', round($params['height'])); - $nodePiece->addChild('Depth', round($params['length'])); - } else { - $nodePiece->addChild('Depth', round($params['length'])); - $nodePiece->addChild('Width', round($params['width'])); - $nodePiece->addChild('Height', round($params['height'])); - } + $nodePiece->addChild('Width', round($params['width'])); + $nodePiece->addChild('Height', round($params['height'])); + $nodePiece->addChild('Depth', round($params['length'])); } $content = []; foreach ($package['items'] as $item) { @@ -1635,51 +1658,34 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') $nodePiece->addChild('PieceContents', substr(implode(',', $content), 0, 34)); } - if (!$originRegion) { - $nodeShipmentDetails->addChild('Weight', sprintf('%.1f', $rawRequest->getPackageWeight())); - $nodeShipmentDetails->addChild('WeightUnit', substr($this->_getWeightUnit(), 0, 1)); - $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); - $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); - $nodeShipmentDetails->addChild('Date', $this->_coreDate->date('Y-m-d')); - $nodeShipmentDetails->addChild('Contents', 'DHL Parcel'); - /** - * The DoorTo Element defines the type of delivery service that applies to the shipment. - * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to - * Door non-compliant) - */ - $nodeShipmentDetails->addChild('DoorTo', 'DD'); - $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); - if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { - $packageType = 'CP'; - } - $nodeShipmentDetails->addChild('PackageType', $packageType); - if ($this->isDutiable($rawRequest->getOrigCountryId(), $rawRequest->getDestCountryId())) { - $nodeShipmentDetails->addChild('IsDutiable', 'Y'); - } - $nodeShipmentDetails->addChild( - 'CurrencyCode', - $this->_storeManager->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() - ); - } else { - if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { - $packageType = 'CP'; - } - $nodeShipmentDetails->addChild('PackageType', $packageType); - $nodeShipmentDetails->addChild('Weight', sprintf('%.3f', $rawRequest->getPackageWeight())); - $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); - $nodeShipmentDetails->addChild('WeightUnit', substr($this->_getWeightUnit(), 0, 1)); - $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); - $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); - - /** - * The DoorTo Element defines the type of delivery service that applies to the shipment. - * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to - * Door non-compliant) - */ - $nodeShipmentDetails->addChild('DoorTo', 'DD'); - $nodeShipmentDetails->addChild('Date', $this->_coreDate->date('Y-m-d')); - $nodeShipmentDetails->addChild('Contents', 'DHL Parcel TEST'); + $nodeShipmentDetails->addChild('Weight', sprintf('%.3f', $rawRequest->getPackageWeight())); + $nodeShipmentDetails->addChild('WeightUnit', substr($this->_getWeightUnit(), 0, 1)); + $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); + $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); + $nodeShipmentDetails->addChild( + 'Date', + $this->_coreDate->date('Y-m-d', strtotime('now + 1day')) + ); + $nodeShipmentDetails->addChild('Contents', 'DHL Parcel'); + + /** + * The DoorTo Element defines the type of delivery service that applies to the shipment. + * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to + * Door non-compliant) + */ + $nodeShipmentDetails->addChild('DoorTo', 'DD'); + $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); + if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { + $packageType = 'CP'; + } + $nodeShipmentDetails->addChild('PackageType', $packageType); + if ($this->isDutiable($rawRequest->getOrigCountryId(), $rawRequest->getDestCountryId())) { + $nodeShipmentDetails->addChild('IsDutiable', 'Y'); } + $nodeShipmentDetails->addChild( + 'CurrencyCode', + $this->_storeManager->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() + ); } /** @@ -1710,12 +1716,15 @@ protected function _getXMLTracking($trackings) '<req:KnownTrackingRequest' . ' xmlns:req="http://www.dhl.com"' . ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' . - ' xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown.xsd" />'; + ' xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown-1.0.xsd"' . + ' schemaVersion="1.0" />'; $xml = $this->_xmlElFactory->create(['data' => $xmlStr]); $requestNode = $xml->addChild('Request', '', ''); $serviceHeaderNode = $requestNode->addChild('ServiceHeader', '', ''); + $serviceHeaderNode->addChild('MessageTime', $this->buildMessageTimestamp()); + $serviceHeaderNode->addChild('MessageReference', $this->buildMessageReference($this->servicePrefixTracking)); $serviceHeaderNode->addChild('SiteID', (string)$this->getConfigData('id')); $serviceHeaderNode->addChild('Password', (string)$this->getConfigData('password')); @@ -1968,8 +1977,61 @@ protected function isDutiable($origCountryId, $destCountryId) { $this->_checkDomesticStatus($origCountryId, $destCountryId); - return - self::DHL_CONTENT_TYPE_NON_DOC == $this->getConfigData('content_type') - || !$this->_isDomestic; + return !$this->_isDomestic; + } + + /** + * Builds a datetime string to be used as the MessageTime in accordance to the expected format. + * + * @param string|null $datetime + * @return string + */ + private function buildMessageTimestamp(string $datetime = null): string + { + return $this->_coreDate->date(\DATE_RFC3339, $datetime); + } + + /** + * Builds a string to be used as the MessageReference. + * + * @param string $servicePrefix + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function buildMessageReference(string $servicePrefix): string + { + $validPrefixes = [ + $this->servicePrefixQuote, + $this->servicePrefixShipval, + $this->servicePrefixTracking, + ]; + + if (!in_array($servicePrefix, $validPrefixes)) { + throw new \Magento\Framework\Exception\LocalizedException( + __("Invalid service prefix \"$servicePrefix\" provided while attempting to build MessageReference") + ); + } + + return str_replace('.', '', uniqid("MAGE_{$servicePrefix}_", true)); + } + + /** + * Builds a string to be used as the request SoftwareName. + * + * @return string + */ + private function buildSoftwareName(): string + { + return substr($this->productMetadata->getName(), 0, 30); + } + + /** + * Builds a string to be used as the request SoftwareVersion. + * + * @return string + */ + private function buildSoftwareVersion(): string + { + return substr($this->productMetadata->getVersion(), 0, 10); } } diff --git a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php index 949311bb65d05..74217c24d3b10 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php @@ -3,17 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Dhl\Test\Unit\Model; use Magento\Dhl\Model\Carrier; use Magento\Dhl\Model\Validator\XmlValidator; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\Filesystem\Directory\Read; use Magento\Framework\Filesystem\Directory\ReadFactory; use Magento\Framework\HTTP\ZendClient; use Magento\Framework\HTTP\ZendClientFactory; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Module\Dir\Reader; +use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Xml\Security; use Magento\Quote\Model\Quote\Address\RateRequest; @@ -32,7 +35,6 @@ use Magento\Store\Model\Website; use PHPUnit_Framework_MockObject_MockObject as MockObject; use Psr\Log\LoggerInterface; -use Magento\Store\Model\ScopeInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -80,14 +82,19 @@ class CarrierTest extends \PHPUnit\Framework\TestCase private $xmlValidator; /** - * @var Request|MockObject + * @var LoggerInterface|MockObject */ - private $request; + private $logger; /** - * @var LoggerInterface|MockObject + * @var DateTime|MockObject */ - private $logger; + private $coreDateMock; + + /** + * @var ProductMetadataInterface + */ + private $productMetadataMock; /** * @inheritdoc @@ -96,35 +103,8 @@ protected function setUp() { $this->objectManager = new ObjectManager($this); - $this->request = $this->getMockBuilder(Request::class) - ->disableOriginalConstructor() - ->setMethods( - [ - 'getPackages', - 'getOrigCountryId', - 'setPackages', - 'setPackageWeight', - 'setPackageValue', - 'setValueWithDiscount', - 'setPackageCustomsValue', - 'setFreeMethodWeight', - 'getPackageWeight', - 'getFreeMethodWeight', - 'getOrderShipment', - ] - ) - ->getMock(); - $this->scope = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $xmlElFactory = $this->getXmlFactory(); - $rateFactory = $this->getRateFactory(); - $rateMethodFactory = $this->getRateMethodFactory(); - $httpClientFactory = $this->getHttpClientFactory(); - $configReader = $this->getConfigReader(); - $readFactory = $this->getReadFactory(); - $storeManager = $this->getStoreManager(); - $this->error = $this->getMockBuilder(Error::class) ->setMethods(['setCarrier', 'setCarrierTitle', 'setErrorMessage']) ->getMock(); @@ -135,31 +115,45 @@ protected function setUp() $this->errorFactory->method('create') ->willReturn($this->error); - $carrierHelper = $this->getCarrierHelper(); - $this->xmlValidator = $this->getMockBuilder(XmlValidator::class) ->disableOriginalConstructor() ->getMock(); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->coreDateMock = $this->getMockBuilder(DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->coreDateMock->method('date') + ->willReturn('currentTime'); + + $this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productMetadataMock->method('getName') + ->willReturn('Software_Product_Name_30_Char_123456789'); + $this->productMetadataMock->method('getVersion') + ->willReturn('10Char_Ver123456789'); + $this->model = $this->objectManager->getObject( Carrier::class, [ 'scopeConfig' => $this->scope, - 'xmlSecurity' => new Security(), - 'logger' => $this->logger, - 'xmlElFactory' => $xmlElFactory, - 'rateFactory' => $rateFactory, 'rateErrorFactory' => $this->errorFactory, - 'rateMethodFactory' => $rateMethodFactory, - 'httpClientFactory' => $httpClientFactory, - 'readFactory' => $readFactory, - 'storeManager' => $storeManager, - 'configReader' => $configReader, - 'carrierHelper' => $carrierHelper, + 'logger' => $this->logger, + 'xmlSecurity' => new Security(), + 'xmlElFactory' => $this->getXmlFactory(), + 'rateFactory' => $this->getRateFactory(), + 'rateMethodFactory' => $this->getRateMethodFactory(), + 'carrierHelper' => $this->getCarrierHelper(), + 'configReader' => $this->getConfigReader(), + 'storeManager' => $this->getStoreManager(), + 'readFactory' => $this->getReadFactory(), + 'httpClientFactory' => $this->getHttpClientFactory(), 'data' => ['id' => 'dhl', 'store' => '1'], 'xmlValidator' => $this->xmlValidator, + 'coreDate' => $this->coreDateMock, + 'productMetadata' => $this->productMetadataMock ] ); } @@ -176,14 +170,14 @@ public function scopeConfigGetValue($path) 'carriers/dhl/shipment_days' => 'Mon,Tue,Wed,Thu,Fri,Sat', 'carriers/dhl/intl_shipment_days' => 'Mon,Tue,Wed,Thu,Fri,Sat', 'carriers/dhl/allowed_methods' => 'IE', - 'carriers/dhl/international_searvice' => 'IE', + 'carriers/dhl/international_service' => 'IE', 'carriers/dhl/gateway_url' => 'https://xmlpi-ea.dhl.com/XMLShippingServlet', 'carriers/dhl/id' => 'some ID', 'carriers/dhl/password' => 'some password', 'carriers/dhl/content_type' => 'N', 'carriers/dhl/nondoc_methods' => '1,3,4,8,P,Q,E,F,H,J,M,V,Y', 'carriers/dhl/showmethod' => 1, - 'carriers/dhl/title' => 'dhl Title', + 'carriers/dhl/title' => 'DHL Title', 'carriers/dhl/specificerrmsg' => 'dhl error message', 'carriers/dhl/unit_of_measure' => 'K', 'carriers/dhl/size' => '1', @@ -191,11 +185,16 @@ public function scopeConfigGetValue($path) 'carriers/dhl/width' => '1.6', 'carriers/dhl/depth' => '1.6', 'carriers/dhl/debug' => 1, - 'shipping/origin/country_id' => 'GB', + 'shipping/origin/country_id' => 'GB' ]; return isset($pathMap[$path]) ? $pathMap[$path] : null; } + /** + * Prepare shipping label content test + * + * @throws \ReflectionException + */ public function testPrepareShippingLabelContent() { $xml = simplexml_load_file( @@ -207,6 +206,8 @@ public function testPrepareShippingLabelContent() } /** + * Prepare shipping label content exception test + * * @dataProvider prepareShippingLabelContentExceptionDataProvider * @expectedException \Magento\Framework\Exception\LocalizedException * @expectedExceptionMessage Unable to retrieve shipping label @@ -217,6 +218,8 @@ public function testPrepareShippingLabelContentException(\SimpleXMLElement $xml) } /** + * Prepare shipping label content exception data provider + * * @return array */ public function prepareShippingLabelContentExceptionDataProvider() @@ -236,8 +239,11 @@ public function prepareShippingLabelContentExceptionDataProvider() } /** + * Invoke prepare shipping label content + * * @param \SimpleXMLElement $xml * @return \Magento\Framework\DataObject + * @throws \ReflectionException */ protected function _invokePrepareShippingLabelContent(\SimpleXMLElement $xml) { @@ -247,8 +253,14 @@ protected function _invokePrepareShippingLabelContent(\SimpleXMLElement $xml) return $method->invoke($model, $xml); } + /** + * Tests that valid rates are returned when sending a quotes request. + */ public function testCollectRates() { + $requestData = require __DIR__ . '/_files/dhl_quote_request_data.php'; + $responseXml = file_get_contents(__DIR__ . '/_files/dhl_quote_response.xml'); + $this->scope->method('getValue') ->willReturnCallback([$this, 'scopeConfigGetValue']); @@ -256,29 +268,44 @@ public function testCollectRates() ->willReturn(true); $this->httpResponse->method('getBody') - ->willReturn(file_get_contents(__DIR__ . '/_files/success_dhl_response_rates.xml')); + ->willReturn($responseXml); - /** @var RateRequest $request */ - $request = $this->objectManager->getObject( - RateRequest::class, - require __DIR__ . '/_files/rates_request_data_dhl.php' - ); + $this->coreDateMock->method('date') + ->willReturnCallback(function () { + return date(\DATE_RFC3339); + }); + + $request = $this->objectManager->getObject(RateRequest::class, $requestData); $reflectionClass = new \ReflectionObject($this->httpClient); $rawPostData = $reflectionClass->getProperty('raw_post_data'); $rawPostData->setAccessible(true); - $this->logger->expects(self::once()) + $this->logger->expects($this->once()) ->method('debug') - ->with(self::stringContains('<SiteID>****</SiteID><Password>****</Password>')); + ->with($this->stringContains('<SiteID>****</SiteID><Password>****</Password>')); + + $expectedRates = require __DIR__ . '/_files/dhl_quote_response_rates.php'; + $actualRates = $this->model->collectRates($request)->getAllRates(); + + self::assertEquals(count($expectedRates), count($actualRates)); + + foreach ($actualRates as $i => $actualRate) { + $actualRate = $actualRate->getData(); + unset($actualRate['method_title']); + self::assertEquals($expectedRates[$i], $actualRate); + } - self::assertNotEmpty($this->model->collectRates($request)->getAllRates()); - self::assertContains('<Weight>18.223</Weight>', $rawPostData->getValue($this->httpClient)); - self::assertContains('<Height>0.630</Height>', $rawPostData->getValue($this->httpClient)); - self::assertContains('<Width>0.630</Width>', $rawPostData->getValue($this->httpClient)); - self::assertContains('<Depth>0.630</Depth>', $rawPostData->getValue($this->httpClient)); + $requestXml = $rawPostData->getValue($this->httpClient); + self::assertContains('<Weight>18.223</Weight>', $requestXml); + self::assertContains('<Height>0.630</Height>', $requestXml); + self::assertContains('<Width>0.630</Width>', $requestXml); + self::assertContains('<Depth>0.630</Depth>', $requestXml); } + /** + * Tests that an error is returned when attempting to collect rates for an inactive shipping method. + */ public function testCollectRatesErrorMessage() { $this->scope->method('getValue') @@ -296,26 +323,81 @@ public function testCollectRatesErrorMessage() $this->assertSame($this->error, $this->model->collectRates($request)); } - public function testCollectRatesFail() + /** + * Test request to shipment sends valid xml values. + * + * @dataProvider requestToShipmentDataProvider + * @param string $origCountryId + * @param string $expectedRegionCode + * @param string $destCountryId + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \ReflectionException + */ + public function testRequestToShipment(string $origCountryId, string $expectedRegionCode, string $destCountryId) { - $this->scope->expects($this->once())->method('isSetFlag')->willReturn(true); + $scopeConfigValueMap = [ + ['carriers/dhl/account', 'store', null, '1234567890'], + ['carriers/dhl/gateway_url', 'store', null, 'https://xmlpi-ea.dhl.com/XMLShippingServlet'], + ['carriers/dhl/id', 'store', null, 'some ID'], + ['carriers/dhl/password', 'store', null, 'some password'], + ['carriers/dhl/content_type', 'store', null, 'N'], + ['carriers/dhl/nondoc_methods', 'store', null, '1,3,4,8,P,Q,E,F,H,J,M,V,Y'], + ['shipping/origin/country_id', 'store', null, $origCountryId], + ]; - $request = new RateRequest(); - $request->setPackageWeight(1); + $this->scope->method('getValue') + ->willReturnMap($scopeConfigValueMap); + + $this->httpResponse->method('getBody') + ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); - $this->assertFalse(false, $this->model->collectRates($request)); + $request = $this->getRequest($origCountryId, $destCountryId); + + $this->logger->method('debug') + ->with($this->stringContains('<SiteID>****</SiteID><Password>****</Password>')); + + $result = $this->model->requestToShipment($request); + + $reflectionClass = new \ReflectionObject($this->httpClient); + $rawPostData = $reflectionClass->getProperty('raw_post_data'); + $rawPostData->setAccessible(true); + + $this->assertNotNull($result); + $requestXml = $rawPostData->getValue($this->httpClient); + $requestElement = new Element($requestXml); + + $messageReference = $requestElement->Request->ServiceHeader->MessageReference->__toString(); + $this->assertStringStartsWith('MAGE_SHIP_', $messageReference); + $this->assertGreaterThanOrEqual(28, strlen($messageReference)); + $this->assertLessThanOrEqual(32, strlen($messageReference)); + $requestElement->Request->ServiceHeader->MessageReference = 'MAGE_SHIP_28TO32_Char_CHECKED'; + + $this->assertXmlStringEqualsXmlString( + $this->getExpectedRequestXml($origCountryId, $destCountryId, $expectedRegionCode)->asXML(), + $requestElement->asXML() + ); } /** - * Test request to shipment sends valid xml values. + * Prepare and retrieve request object + * + * @param string $origCountryId + * @param string $destCountryId + * @return Request|MockObject */ - public function testRequestToShipment() + private function getRequest(string $origCountryId, string $destCountryId) { - $this->scope->method('getValue') - ->willReturnCallback([$this, 'scopeConfigGetValue']); + $order = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $order->method('getSubtotal') + ->willReturn('10.00'); - $this->httpResponse->method('getBody') - ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); + $shipment = $this->getMockBuilder(Order\Shipment::class) + ->disableOriginalConstructor() + ->getMock(); + $shipment->method('getOrder') + ->willReturn($order); $packages = [ 'package' => [ @@ -334,130 +416,106 @@ public function testRequestToShipment() 'name' => 'item_name', ], ], - ] + ], ]; - $order = $this->getMockBuilder(Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order->method('getSubtotal') - ->willReturn('10.00'); + $methods = [ + 'getPackages' => $packages, + 'getOrigCountryId' => $origCountryId, + 'getDestCountryId' => $destCountryId, + 'getShipperAddressCountryCode' => $origCountryId, + 'getRecipientAddressCountryCode' => $destCountryId, + 'setPackages' => null, + 'setPackageWeight' => null, + 'setPackageValue' => null, + 'setValueWithDiscount' => null, + 'setPackageCustomsValue' => null, + 'setFreeMethodWeight' => null, + 'getPackageWeight' => '0.454000000001', + 'getFreeMethodWeight' => '0.454000000001', + 'getOrderShipment' => $shipment, + ]; - $shipment = $this->getMockBuilder(Order\Shipment::class) + /** @var Request|MockObject $request */ + $request = $this->getMockBuilder(Request::class) ->disableOriginalConstructor() + ->setMethods(array_keys($methods)) ->getMock(); - $shipment->method('getOrder') - ->willReturn($order); - - $this->request->method('getPackages') - ->willReturn($packages); - $this->request->method('getOrigCountryId') - ->willReturn('GB'); - $this->request->method('setPackages') - ->willReturnSelf(); - $this->request->method('setPackageWeight') - ->willReturnSelf(); - $this->request->method('setPackageValue') - ->willReturnSelf(); - $this->request->method('setValueWithDiscount') - ->willReturnSelf(); - $this->request->method('setPackageCustomsValue') - ->willReturnSelf(); - $this->request->method('setFreeMethodWeight') - ->willReturnSelf(); - $this->request->method('getPackageWeight') - ->willReturn('0.454000000001'); - $this->request->method('getFreeMethodWeight') - ->willReturn('0.454000000001'); - $this->request->method('getOrderShipment') - ->willReturn($shipment); - - $this->logger->method('debug') - ->with(self::stringContains('<SiteID>****</SiteID><Password>****</Password>')); - $result = $this->model->requestToShipment($this->request); + foreach ($methods as $method => $return) { + $return ? $request->method($method)->willReturn($return) : $request->method($method)->willReturnSelf(); + } - $reflectionClass = new \ReflectionObject($this->httpClient); - $rawPostData = $reflectionClass->getProperty('raw_post_data'); - $rawPostData->setAccessible(true); - - $this->assertNotNull($result); - $this->assertContains('<Weight>0.454</Weight>', $rawPostData->getValue($this->httpClient)); + return $request; } /** - * Test that shipping label request for origin country from AP region doesn't contain restricted fields. + * Prepare and retrieve expected request xml element + * + * @param string $origCountryId + * @param string $destCountryId + * @return Element */ - public function testShippingLabelRequestForAsiaPacificRegion() + private function getExpectedRequestXml(string $origCountryId, string $destCountryId, string $regionCode) { - $this->scope->method('getValue') - ->willReturnMap( - [ - ['shipping/origin/country_id', ScopeInterface::SCOPE_STORE, null, 'SG'], - ['carriers/dhl/gateway_url', ScopeInterface::SCOPE_STORE, null, 'https://xmlpi-ea.dhl.com'], - ] - ); + $requestXmlPath = $origCountryId == $destCountryId + ? '/_files/domestic_shipment_request.xml' + : '/_files/shipment_request.xml'; - $this->httpResponse->method('getBody') - ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); + $expectedRequestElement = new Element(file_get_contents(__DIR__ . $requestXmlPath)); - $packages = [ - 'package' => [ - 'params' => [ - 'width' => '1', - 'length' => '1', - 'height' => '1', - 'dimension_units' => 'INCH', - 'weight_units' => 'POUND', - 'weight' => '0.45', - 'customs_value' => '10.00', - 'container' => Carrier::DHL_CONTENT_TYPE_NON_DOC, - ], - 'items' => [ - 'item1' => [ - 'name' => 'item_name', - ], - ], - ] - ]; + $expectedRequestElement->Consignee->CountryCode = $destCountryId; + $expectedRequestElement->Consignee->CountryName = $this->getCountryName($destCountryId); - $this->request->method('getPackages')->willReturn($packages); - $this->request->method('getOrigCountryId')->willReturn('SG'); - $this->request->method('setPackages')->willReturnSelf(); - $this->request->method('setPackageWeight')->willReturnSelf(); - $this->request->method('setPackageValue')->willReturnSelf(); - $this->request->method('setValueWithDiscount')->willReturnSelf(); - $this->request->method('setPackageCustomsValue')->willReturnSelf(); + $expectedRequestElement->Shipper->CountryCode = $origCountryId; + $expectedRequestElement->Shipper->CountryName = $this->getCountryName($origCountryId); - $result = $this->model->requestToShipment($this->request); + $expectedRequestElement->RegionCode = $regionCode; - $reflectionClass = new \ReflectionObject($this->httpClient); - $rawPostData = $reflectionClass->getProperty('raw_post_data'); - $rawPostData->setAccessible(true); + return $expectedRequestElement; + } - $this->assertNotNull($result); - $requestXml = $rawPostData->getValue($this->httpClient); + /** + * Get Country Name by Country Code + * + * @param string $countryCode + * @return string + */ + private function getCountryName($countryCode) + { + $countryNames = [ + 'US' => 'United States of America', + 'SG' => 'Singapore', + 'GB' => 'United Kingdom', + 'DE' => 'Germany', + ]; + return $countryNames[$countryCode]; + } - $this->assertNotContains( - 'NewShipper', - $requestXml, - 'NewShipper is restricted field for AP region' - ); - $this->assertNotContains( - 'Division', - $requestXml, - 'Division is restricted field for AP region' - ); - $this->assertNotContains( - 'RegisteredAccount', - $requestXml, - 'RegisteredAccount is restricted field for AP region' - ); + /** + * Data provider to testRequestToShipment + * + * @return array + */ + public function requestToShipmentDataProvider() + { + return [ + [ + 'GB', 'EU', 'US' + ], + [ + 'SG', 'AP', 'US' + ], + [ + 'DE', 'EU', 'DE' + ] + ]; } /** - * @dataProvider dhlProductsDataProvider + * Get DHL products test * + * @dataProvider dhlProductsDataProvider * @param string $docType * @param array $products */ @@ -467,9 +525,11 @@ public function testGetDhlProducts(string $docType, array $products) } /** + * DHL products data provider + * * @return array */ - public function dhlProductsDataProvider() : array + public function dhlProductsDataProvider(): array { return [ 'doc' => [ @@ -495,7 +555,7 @@ public function dhlProductsDataProvider() : array 'S' => 'Same day', 'T' => 'Express 12:00', 'X' => 'Express envelope', - ] + ], ], 'non-doc' => [ 'docType' => Carrier::DHL_CONTENT_TYPE_NON_DOC, @@ -513,8 +573,120 @@ public function dhlProductsDataProvider() : array 'M' => 'Express 10:30', 'V' => 'Europack', 'Y' => 'Express 12:00', - ] - ] + ], + ], + ]; + } + + /** + * Tests that the built MessageReference string is of the appropriate format. + * + * @dataProvider buildMessageReferenceDataProvider + * @param string $servicePrefix + * + * @return void + */ + public function testBuildMessageReference(string $servicePrefix) + { + $method = new \ReflectionMethod($this->model, 'buildMessageReference'); + $method->setAccessible(true); + + $messageReference = $method->invoke($this->model, $servicePrefix); + $this->assertGreaterThanOrEqual(28, strlen($messageReference)); + $this->assertLessThanOrEqual(32, strlen($messageReference)); + } + + /** + * Build message reference data provider + * + * @return array + */ + public function buildMessageReferenceDataProvider(): array + { + return [ + 'quote_prefix' => ['QUOT'], + 'shipval_prefix' => ['SHIP'], + 'tracking_prefix' => ['TRCK'], + ]; + } + + /** + * Tests that an exception is thrown when an invalid service prefix is provided. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid service prefix + * + * @return void + */ + public function testBuildMessageReferenceInvalidPrefix() + { + $method = new \ReflectionMethod($this->model, 'buildMessageReference'); + $method->setAccessible(true); + + $method->invoke($this->model, 'INVALID'); + } + + /** + * Tests that the built software name string is of the appropriate format. + * + * @dataProvider buildSoftwareNameDataProvider + * @param string $productName + * + * @return void + */ + public function testBuildSoftwareName(string $productName) + { + $method = new \ReflectionMethod($this->model, 'buildSoftwareName'); + $method->setAccessible(true); + + $this->productMetadataMock->method('getName')->willReturn($productName); + + $softwareName = $method->invoke($this->model); + $this->assertLessThanOrEqual(30, strlen($softwareName)); + } + + /** + * Data provider for testBuildSoftwareName + * + * @return array + */ + public function buildSoftwareNameDataProvider(): array + { + return [ + 'valid_length' => ['Magento'], + 'exceeds_length' => ['Product_Name_Longer_Than_30_Char'], + ]; + } + + /** + * Tests that the built software version string is of the appropriate format. + * + * @dataProvider buildSoftwareVersionProvider + * @param string $productVersion + * + * @return void + */ + public function testBuildSoftwareVersion(string $productVersion) + { + $method = new \ReflectionMethod($this->model, 'buildSoftwareVersion'); + $method->setAccessible(true); + + $this->productMetadataMock->method('getVersion')->willReturn($productVersion); + + $softwareVersion = $method->invoke($this->model); + $this->assertLessThanOrEqual(10, strlen($softwareVersion)); + } + + /** + * Data provider for testBuildSoftwareVersion + * + * @return array + */ + public function buildSoftwareVersionProvider(): array + { + return [ + 'valid_length' => ['2.3.1'], + 'exceeds_length' => ['dev-MC-1000'], ]; } @@ -576,19 +748,25 @@ private function getRateMethodFactory(): MockObject ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $rateMethod = $this->getMockBuilder(Method::class) - ->disableOriginalConstructor() - ->setMethods(['setPrice']) - ->getMock(); - $rateMethod->method('setPrice') - ->willReturnSelf(); + $rateMethodFactory->method('create') - ->willReturn($rateMethod); + ->willReturnCallback(function () { + $rateMethod = $this->getMockBuilder(Method::class) + ->disableOriginalConstructor() + ->setMethods(['setPrice']) + ->getMock(); + $rateMethod->method('setPrice') + ->willReturnSelf(); + + return $rateMethod; + }); return $rateMethodFactory; } /** + * Get config reader + * * @return MockObject */ private function getConfigReader(): MockObject @@ -603,6 +781,8 @@ private function getConfigReader(): MockObject } /** + * Get read factory + * * @return MockObject */ private function getReadFactory(): MockObject @@ -621,6 +801,8 @@ private function getReadFactory(): MockObject } /** + * Get store manager + * * @return MockObject */ private function getStoreManager(): MockObject @@ -642,6 +824,8 @@ private function getStoreManager(): MockObject } /** + * Get carrier helper + * * @return CarrierHelper */ private function getCarrierHelper(): CarrierHelper @@ -652,7 +836,7 @@ private function getCarrierHelper(): CarrierHelper $carrierHelper = $this->objectManager->getObject( CarrierHelper::class, [ - 'localeResolver' => $localeResolver + 'localeResolver' => $localeResolver, ] ); @@ -660,6 +844,8 @@ private function getCarrierHelper(): CarrierHelper } /** + * Get HTTP client factory + * * @return MockObject */ private function getHttpClientFactory(): MockObject diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/apregion_shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/apregion_shipment_request.xml new file mode 100644 index 0000000000000..91f8372cd8dbb --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/apregion_shipment_request.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentValidateRequestAP xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com ship-val-req_AP.xsd"> + <Request xmlns=""> + <ServiceHeader> + <SiteID>some ID</SiteID> + <Password>some password</Password> + </ServiceHeader> + </Request> + <LanguageCode xmlns="">EN</LanguageCode> + <PiecesEnabled xmlns="">Y</PiecesEnabled> + <Billing xmlns=""> + <ShipperAccountNumber>1234567890</ShipperAccountNumber> + <ShippingPaymentType>S</ShippingPaymentType> + <BillingAccountNumber>1234567890</BillingAccountNumber> + <DutyPaymentType>S</DutyPaymentType> + <DutyAccountNumber>1234567890</DutyAccountNumber> + </Billing> + <Consignee xmlns=""> + <CompanyName/> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact> + <PersonName/> + <PhoneNumber/> + </Contact> + </Consignee> + <Commodity xmlns=""> + <CommodityCode>1</CommodityCode> + </Commodity> + <Dutiable xmlns=""> + <DeclaredValue>10.00</DeclaredValue> + <DeclaredCurrency>USD</DeclaredCurrency> + </Dutiable> + <Reference xmlns=""> + <ReferenceID>shipment reference</ReferenceID> + <ReferenceType>St</ReferenceType> + </Reference> + <ShipmentDetails xmlns=""> + <NumberOfPieces>1</NumberOfPieces> + <CurrencyCode>USD</CurrencyCode> + <Pieces xmlns=""> + <Piece xmlns=""> + <PieceID>1</PieceID> + <PackageType>CP</PackageType> + <Weight>0.5</Weight> + <Depth>3</Depth> + <Width>3</Width> + <Height>3</Height> + <PieceContents>item_name</PieceContents> + </Piece> + </Pieces> + <PackageType>CP</PackageType> + <Weight>0.454</Weight> + <DimensionUnit>C</DimensionUnit> + <WeightUnit>K</WeightUnit> + <GlobalProductCode/> + <LocalProductCode/> + <DoorTo>DD</DoorTo> + <Date/> + <Contents>DHL Parcel TEST</Contents> + </ShipmentDetails> + <Shipper xmlns=""> + <ShipperID>1234567890</ShipperID> + <CompanyName/> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact xmlns=""> + <PersonName/> + <PhoneNumber/> + </Contact> + </Shipper> + <LabelImageFormat xmlns="">PDF</LabelImageFormat> +</req:ShipmentValidateRequestAP> diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml index 3f28111f229d1..fb9188fa37653 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="UTF-8"?> +<?xml version="1.0"?> <!-- /** * Copyright © Magento, Inc. All rights reserved. @@ -83,7 +83,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Austria</name> <domestic>1</domestic> </AT> @@ -132,7 +132,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Belgium</name> <domestic>1</domestic> </BE> @@ -146,7 +146,7 @@ <currency>BGN</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Bulgaria</name> <domestic>1</domestic> </BG> @@ -257,7 +257,7 @@ <currency>CHF</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Switzerland</name> </CH> <CI> @@ -331,7 +331,7 @@ <currency>CZK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Czech Republic, The</name> <domestic>1</domestic> </CZ> @@ -339,7 +339,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Germany</name> <domestic>1</domestic> </DE> @@ -353,7 +353,7 @@ <currency>DKK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Denmark</name> <domestic>1</domestic> </DK> @@ -389,7 +389,7 @@ <currency>EEK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Estonia</name> <domestic>1</domestic> </EE> @@ -410,7 +410,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Spain</name> <domestic>1</domestic> </ES> @@ -424,7 +424,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Finland</name> <domestic>1</domestic> </FI> @@ -457,7 +457,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>France</name> <domestic>1</domestic> </FR> @@ -471,7 +471,7 @@ <currency>GBP</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>United Kingdom</name> <domestic>1</domestic> </GB> @@ -549,7 +549,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Greece</name> <domestic>1</domestic> </GR> @@ -612,7 +612,7 @@ <currency>HUF</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Hungary</name> <domestic>1</domestic> </HU> @@ -633,7 +633,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Ireland, Republic Of</name> <domestic>1</domestic> </IE> @@ -668,14 +668,14 @@ <currency>ISK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Iceland</name> </IS> <IT> <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Italy</name> <domestic>1</domestic> </IT> @@ -834,7 +834,7 @@ <currency>LTL</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Lithuania</name> <domestic>1</domestic> </LT> @@ -842,7 +842,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Luxembourg</name> <domestic>1</domestic> </LU> @@ -850,7 +850,7 @@ <currency>LVL</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Latvia</name> <domestic>1</domestic> </LV> @@ -1039,7 +1039,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Netherlands, The</name> <domestic>1</domestic> </NL> @@ -1047,7 +1047,7 @@ <currency>NOK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Norway</name> </NO> <NP> @@ -1127,7 +1127,7 @@ <currency>PLN</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Poland</name> <domestic>1</domestic> </PL> @@ -1142,7 +1142,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Portugal</name> <domestic>1</domestic> </PT> @@ -1177,7 +1177,7 @@ <currency>RON</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Romania</name> <domestic>1</domestic> </RO> @@ -1231,7 +1231,7 @@ <currency>SEK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Sweden</name> <domestic>1</domestic> </SE> @@ -1246,7 +1246,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Slovenia</name> <domestic>1</domestic> </SI> @@ -1254,7 +1254,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Slovakia</name> <domestic>1</domestic> </SK> @@ -1417,7 +1417,7 @@ <weight_unit>LB</weight_unit> <measure_unit>IN</measure_unit> <region>AM</region> - <name>United States Of America</name> + <name>United States of America</name> </US> <UY> <currency>UYU</currency> diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_request_data.php b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_request_data.php new file mode 100644 index 0000000000000..0de635388e583 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_request_data.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'data' => [ + 'dest_country_id' => 'DE', + 'dest_region_id' => '82', + 'dest_region_code' => 'BER', + 'dest_street' => 'Turmstraße 17', + 'dest_city' => 'Berlin', + 'dest_postcode' => '10559', + 'dest_postal' => '10559', + 'package_value' => '5', + 'package_value_with_discount' => '5', + 'package_weight' => '8.2657', + 'package_qty' => '1', + 'package_physical_value' => '5', + 'free_method_weight' => '5', + 'store_id' => '1', + 'website_id' => '1', + 'free_shipping' => '0', + 'limit_carrier' => null, + 'base_subtotal_incl_tax' => '5', + 'orig_country_id' => 'US', + 'country_id' => 'US', + 'orig_region_id' => '12', + 'orig_city' => 'Fremont', + 'orig_postcode' => '94538', + 'dhl_id' => 'MAGEN_8501', + 'dhl_password' => 'QR2GO1U74X', + 'dhl_account' => '799909537', + 'dhl_shipping_intl_key' => '54233F2B2C4E5C4B4C5E5A59565530554B405641475D5659', + 'girth' => null, + 'height' => null, + 'length' => null, + 'width' => null, + 'weight' => 1, + 'dhl_shipment_type' => 'P', + 'dhl_duitable' => 0, + 'dhl_duty_payment_type' => 'R', + 'dhl_content_desc' => 'Big Box', + 'limit_method' => 'IE', + 'ship_date' => '2014-01-09', + 'action' => 'RateEstimate', + 'all_items' => [], + ], +]; diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response.xml new file mode 100644 index 0000000000000..349cfbd4f1ce5 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response.xml @@ -0,0 +1,518 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<res:DCTResponse xmlns:res="http://www.dhl.com" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.dhl.com DCT-Response_global-2.0.xsd"> + <GetQuoteResponse> + <Response> + <ServiceHeader> + <MessageTime>2014-01-09T12:13:29.498+00:00</MessageTime> + <SiteID>EvgeniyUSA</SiteID> + </ServiceHeader> + </Response> + <BkgDetails> + <QtdShp> + <OriginServiceArea> + <FacilityCode>NUQ</FacilityCode> + <ServiceAreaCode>NUQ</ServiceAreaCode> + </OriginServiceArea> + <DestinationServiceArea> + <FacilityCode>BER</FacilityCode> + <ServiceAreaCode>BER</ServiceAreaCode> + </DestinationServiceArea> + <GlobalProductCode>E</GlobalProductCode> + <LocalProductCode>E</LocalProductCode> + <ProductShortName>EXPRESS 9:00</ProductShortName> + <LocalProductName>EXPRESS 9:00 NONDOC</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>N</POfferedCustAgreement> + <TransInd>Y</TransInd> + <PickupDate>2014-01-09</PickupDate> + <PickupCutoffTime>PT16H15M</PickupCutoffTime> + <BookingTime>PT15H15M</BookingTime> + <CurrencyCode>USD</CurrencyCode> + <ExchangeRate>1.000000</ExchangeRate> + <WeightCharge>42.060</WeightCharge> + <WeightChargeTax>0.000</WeightChargeTax> + <TotalTransitDays>2</TotalTransitDays> + <PickupPostalLocAddDays>0</PickupPostalLocAddDays> + <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> + <DeliveryDate> + <DlvyDateTime>2014-01-13 11:59:00</DlvyDateTime> + <DeliveryDateTimeOffset>+00:00</DeliveryDateTimeOffset> + </DeliveryDate> + <DeliveryTime>PT9H</DeliveryTime> + <DimensionalWeight>2.205</DimensionalWeight> + <WeightUnit>LB</WeightUnit> + <PickupDayOfWeekNum>4</PickupDayOfWeekNum> + <DestinationDayOfWeekNum>1</DestinationDayOfWeekNum> + <QtdShpExChrg> + <SpecialServiceType>FF</SpecialServiceType> + <LocalServiceType>FF</LocalServiceType> + <GlobalServiceName>FUEL SURCHARGE</GlobalServiceName> + <LocalServiceTypeName>FUEL SURCHARGE</LocalServiceTypeName> + <ChargeCodeType>SCH</ChargeCodeType> + <CurrencyCode>USD</CurrencyCode> + <ChargeValue>3.790</ChargeValue> + <QtdSExtrChrgInAdCur> + <ChargeValue>3.790</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BILLC</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + <QtdSExtrChrgInAdCur> + <ChargeValue>3.790</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>PULCL</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + <QtdSExtrChrgInAdCur> + <ChargeValue>3.790</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BASEC</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + </QtdShpExChrg> + <PricingDate>2014-01-09</PricingDate> + <ShippingCharge>45.850</ShippingCharge> + <TotalTaxAmount>0.000</TotalTaxAmount> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BILLC</CurrencyRoleTypeCode> + <WeightCharge>42.060</WeightCharge> + <TotalAmount>45.850</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>PULCL</CurrencyRoleTypeCode> + <WeightCharge>42.060</WeightCharge> + <TotalAmount>45.850</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BASEC</CurrencyRoleTypeCode> + <WeightCharge>42.060</WeightCharge> + <TotalAmount>45.850</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <PickupWindowEarliestTime>09:00:00</PickupWindowEarliestTime> + <PickupWindowLatestTime>17:00:00</PickupWindowLatestTime> + <BookingCutoffOffset>PT1H</BookingCutoffOffset> + </QtdShp> + <QtdShp> + <OriginServiceArea> + <FacilityCode>NUQ</FacilityCode> + <ServiceAreaCode>NUQ</ServiceAreaCode> + </OriginServiceArea> + <DestinationServiceArea> + <FacilityCode>BER</FacilityCode> + <ServiceAreaCode>BER</ServiceAreaCode> + </DestinationServiceArea> + <GlobalProductCode>Q</GlobalProductCode> + <LocalProductCode>Q</LocalProductCode> + <ProductShortName>MEDICAL EXPRESS</ProductShortName> + <LocalProductName>MEDICAL EXPRESS</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>Y</POfferedCustAgreement> + <TransInd>N</TransInd> + <PickupDate>2014-01-09</PickupDate> + <PickupCutoffTime>PT16H15M</PickupCutoffTime> + <BookingTime>PT15H15M</BookingTime> + <CurrencyCode>USD</CurrencyCode> + <ExchangeRate>1.000000</ExchangeRate> + <WeightCharge>32.350</WeightCharge> + <WeightChargeTax>0.000</WeightChargeTax> + <TotalTransitDays>2</TotalTransitDays> + <PickupPostalLocAddDays>0</PickupPostalLocAddDays> + <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> + <DeliveryDate> + <DlvyDateTime>2014-01-13 11:59:00</DlvyDateTime> + <DeliveryDateTimeOffset>+00:00</DeliveryDateTimeOffset> + </DeliveryDate> + <DeliveryTime>PT9H</DeliveryTime> + <DimensionalWeight>2.205</DimensionalWeight> + <WeightUnit>LB</WeightUnit> + <PickupDayOfWeekNum>4</PickupDayOfWeekNum> + <DestinationDayOfWeekNum>1</DestinationDayOfWeekNum> + <QtdShpExChrg> + <SpecialServiceType>FF</SpecialServiceType> + <LocalServiceType>FF</LocalServiceType> + <GlobalServiceName>FUEL SURCHARGE</GlobalServiceName> + <LocalServiceTypeName>FUEL SURCHARGE</LocalServiceTypeName> + <ChargeCodeType>SCH</ChargeCodeType> + <CurrencyCode>USD</CurrencyCode> + <ChargeValue>2.910</ChargeValue> + <QtdSExtrChrgInAdCur> + <ChargeValue>2.910</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BILLC</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + <QtdSExtrChrgInAdCur> + <ChargeValue>2.910</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>PULCL</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + <QtdSExtrChrgInAdCur> + <ChargeValue>2.910</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BASEC</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + </QtdShpExChrg> + <PricingDate>2014-01-09</PricingDate> + <ShippingCharge>35.260</ShippingCharge> + <TotalTaxAmount>0.000</TotalTaxAmount> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BILLC</CurrencyRoleTypeCode> + <WeightCharge>32.350</WeightCharge> + <TotalAmount>35.260</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>PULCL</CurrencyRoleTypeCode> + <WeightCharge>32.350</WeightCharge> + <TotalAmount>35.260</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BASEC</CurrencyRoleTypeCode> + <WeightCharge>32.350</WeightCharge> + <TotalAmount>35.260</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <PickupWindowEarliestTime>09:00:00</PickupWindowEarliestTime> + <PickupWindowLatestTime>17:00:00</PickupWindowLatestTime> + <BookingCutoffOffset>PT1H</BookingCutoffOffset> + </QtdShp> + <QtdShp> + <OriginServiceArea> + <FacilityCode>NUQ</FacilityCode> + <ServiceAreaCode>NUQ</ServiceAreaCode> + </OriginServiceArea> + <DestinationServiceArea> + <FacilityCode>BER</FacilityCode> + <ServiceAreaCode>BER</ServiceAreaCode> + </DestinationServiceArea> + <GlobalProductCode>Y</GlobalProductCode> + <LocalProductCode>Y</LocalProductCode> + <ProductShortName>EXPRESS 12:00</ProductShortName> + <LocalProductName>EXPRESS 12:00 NONDOC</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>N</POfferedCustAgreement> + <TransInd>Y</TransInd> + <PickupDate>2014-01-09</PickupDate> + <PickupCutoffTime>PT16H15M</PickupCutoffTime> + <BookingTime>PT15H15M</BookingTime> + <CurrencyCode>USD</CurrencyCode> + <ExchangeRate>1.000000</ExchangeRate> + <WeightCharge>34.290</WeightCharge> + <WeightChargeTax>0.000</WeightChargeTax> + <TotalTransitDays>2</TotalTransitDays> + <PickupPostalLocAddDays>0</PickupPostalLocAddDays> + <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> + <DeliveryDate> + <DlvyDateTime>2014-01-13 11:59:00</DlvyDateTime> + <DeliveryDateTimeOffset>+00:00</DeliveryDateTimeOffset> + </DeliveryDate> + <DeliveryTime>PT12H</DeliveryTime> + <DimensionalWeight>2.205</DimensionalWeight> + <WeightUnit>LB</WeightUnit> + <PickupDayOfWeekNum>4</PickupDayOfWeekNum> + <DestinationDayOfWeekNum>1</DestinationDayOfWeekNum> + <QtdShpExChrg> + <SpecialServiceType>FF</SpecialServiceType> + <LocalServiceType>FF</LocalServiceType> + <GlobalServiceName>FUEL SURCHARGE</GlobalServiceName> + <LocalServiceTypeName>FUEL SURCHARGE</LocalServiceTypeName> + <ChargeCodeType>SCH</ChargeCodeType> + <CurrencyCode>USD</CurrencyCode> + <ChargeValue>3.090</ChargeValue> + <QtdSExtrChrgInAdCur> + <ChargeValue>3.090</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BILLC</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + <QtdSExtrChrgInAdCur> + <ChargeValue>3.090</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>PULCL</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + <QtdSExtrChrgInAdCur> + <ChargeValue>3.090</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BASEC</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + </QtdShpExChrg> + <PricingDate>2014-01-09</PricingDate> + <ShippingCharge>37.380</ShippingCharge> + <TotalTaxAmount>0.000</TotalTaxAmount> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BILLC</CurrencyRoleTypeCode> + <WeightCharge>34.290</WeightCharge> + <TotalAmount>37.380</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>PULCL</CurrencyRoleTypeCode> + <WeightCharge>34.290</WeightCharge> + <TotalAmount>37.380</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BASEC</CurrencyRoleTypeCode> + <WeightCharge>34.290</WeightCharge> + <TotalAmount>37.380</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <PickupWindowEarliestTime>09:00:00</PickupWindowEarliestTime> + <PickupWindowLatestTime>17:00:00</PickupWindowLatestTime> + <BookingCutoffOffset>PT1H</BookingCutoffOffset> + </QtdShp> + <QtdShp> + <OriginServiceArea> + <FacilityCode>NUQ</FacilityCode> + <ServiceAreaCode>NUQ</ServiceAreaCode> + </OriginServiceArea> + <DestinationServiceArea> + <FacilityCode>BER</FacilityCode> + <ServiceAreaCode>BER</ServiceAreaCode> + </DestinationServiceArea> + <GlobalProductCode>3</GlobalProductCode> + <LocalProductCode>3</LocalProductCode> + <ProductShortName>B2C</ProductShortName> + <LocalProductName>EXPRESS WORLDWIDE (B2C)</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>Y</POfferedCustAgreement> + <TransInd>N</TransInd> + <PickupDate>2014-01-09</PickupDate> + <PickupCutoffTime>PT16H15M</PickupCutoffTime> + <BookingTime>PT15H15M</BookingTime> + <ExchangeRate>1.000000</ExchangeRate> + <WeightCharge>0</WeightCharge> + <WeightChargeTax>0.000</WeightChargeTax> + <TotalTransitDays>2</TotalTransitDays> + <PickupPostalLocAddDays>0</PickupPostalLocAddDays> + <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> + <DeliveryDate> + <DlvyDateTime>2014-01-13 11:59:00</DlvyDateTime> + <DeliveryDateTimeOffset>+00:00</DeliveryDateTimeOffset> + </DeliveryDate> + <DeliveryTime>PT23H59M</DeliveryTime> + <DimensionalWeight>2.205</DimensionalWeight> + <WeightUnit>LB</WeightUnit> + <PickupDayOfWeekNum>4</PickupDayOfWeekNum> + <DestinationDayOfWeekNum>1</DestinationDayOfWeekNum> + <PricingDate>2014-01-09</PricingDate> + <ShippingCharge>0.000</ShippingCharge> + <TotalTaxAmount>0.000</TotalTaxAmount> + <QtdSInAdCur> + <CurrencyRoleTypeCode>BILLC</CurrencyRoleTypeCode> + <WeightCharge>0</WeightCharge> + <TotalAmount>0.000</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>PULCL</CurrencyRoleTypeCode> + <WeightCharge>0</WeightCharge> + <TotalAmount>0.000</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BASEC</CurrencyRoleTypeCode> + <WeightCharge>0</WeightCharge> + <TotalAmount>0.000</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <PickupWindowEarliestTime>09:00:00</PickupWindowEarliestTime> + <PickupWindowLatestTime>17:00:00</PickupWindowLatestTime> + <BookingCutoffOffset>PT1H</BookingCutoffOffset> + </QtdShp> + <QtdShp> + <GlobalProductCode>P</GlobalProductCode> + <LocalProductCode>P</LocalProductCode> + <ProductShortName>EXPRESS WORLDWIDE</ProductShortName> + <LocalProductName>EXPRESS WORLDWIDE NONDOC</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>N</POfferedCustAgreement> + <TransInd>Y</TransInd> + <PickupDate>2014-01-09</PickupDate> + <PickupCutoffTime>PT16H15M</PickupCutoffTime> + <BookingTime>PT15H15M</BookingTime> + <CurrencyCode>USD</CurrencyCode> + <ExchangeRate>1.000000</ExchangeRate> + <WeightCharge>32.350</WeightCharge> + <WeightChargeTax>0.000</WeightChargeTax> + <TotalTransitDays>2</TotalTransitDays> + <PickupPostalLocAddDays>0</PickupPostalLocAddDays> + <DeliveryPostalLocAddDays>0</DeliveryPostalLocAddDays> + <PickupNonDHLCourierCode /> + <DeliveryNonDHLCourierCode /> + <DeliveryDate>2014-01-13</DeliveryDate> + <DeliveryTime>PT23H59M</DeliveryTime> + <DimensionalWeight>2.205</DimensionalWeight> + <WeightUnit>LB</WeightUnit> + <PickupDayOfWeekNum>4</PickupDayOfWeekNum> + <DestinationDayOfWeekNum>1</DestinationDayOfWeekNum> + <QtdShpExChrg> + <SpecialServiceType>FF</SpecialServiceType> + <LocalServiceType>FF</LocalServiceType> + <GlobalServiceName>FUEL SURCHARGE</GlobalServiceName> + <LocalServiceTypeName>FUEL SURCHARGE</LocalServiceTypeName> + <ChargeCodeType>SCH</ChargeCodeType> + <CurrencyCode>USD</CurrencyCode> + <ChargeValue>2.910</ChargeValue> + <QtdSExtrChrgInAdCur> + <ChargeValue>2.910</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BILLC</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + <QtdSExtrChrgInAdCur> + <ChargeValue>2.910</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>PULCL</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + <QtdSExtrChrgInAdCur> + <ChargeValue>2.910</ChargeValue> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BASEC</CurrencyRoleTypeCode> + </QtdSExtrChrgInAdCur> + </QtdShpExChrg> + <PricingDate>2014-01-09</PricingDate> + <ShippingCharge>35.260</ShippingCharge> + <TotalTaxAmount>0.000</TotalTaxAmount> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BILLC</CurrencyRoleTypeCode> + <WeightCharge>32.350</WeightCharge> + <TotalAmount>35.260</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>PULCL</CurrencyRoleTypeCode> + <WeightCharge>32.350</WeightCharge> + <TotalAmount>35.260</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + <QtdSInAdCur> + <CurrencyCode>USD</CurrencyCode> + <CurrencyRoleTypeCode>BASEC</CurrencyRoleTypeCode> + <WeightCharge>32.350</WeightCharge> + <TotalAmount>35.260</TotalAmount> + <TotalTaxAmount>0.000</TotalTaxAmount> + <WeightChargeTax>0.000</WeightChargeTax> + </QtdSInAdCur> + </QtdShp> + </BkgDetails> + <Srvs> + <Srv> + <GlobalProductCode>E</GlobalProductCode> + <MrkSrv> + <LocalProductCode>E</LocalProductCode> + <ProductShortName>EXPRESS 9:00</ProductShortName> + <LocalProductName>EXPRESS 9:00 NONDOC</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>N</POfferedCustAgreement> + <TransInd>Y</TransInd> + </MrkSrv> + <MrkSrv> + <LocalServiceType>FF</LocalServiceType> + <GlobalServiceName>FUEL SURCHARGE</GlobalServiceName> + <LocalServiceTypeName>FUEL SURCHARGE</LocalServiceTypeName> + <ChargeCodeType>SCH</ChargeCodeType> + <MrkSrvInd>N</MrkSrvInd> + </MrkSrv> + </Srv> + <Srv> + <GlobalProductCode>Q</GlobalProductCode> + <MrkSrv> + <LocalProductCode>Q</LocalProductCode> + <ProductShortName>MEDICAL EXPRESS</ProductShortName> + <LocalProductName>MEDICAL EXPRESS</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>Y</POfferedCustAgreement> + <TransInd>N</TransInd> + </MrkSrv> + <MrkSrv> + <LocalServiceType>FF</LocalServiceType> + <GlobalServiceName>FUEL SURCHARGE</GlobalServiceName> + <LocalServiceTypeName>FUEL SURCHARGE</LocalServiceTypeName> + <ChargeCodeType>SCH</ChargeCodeType> + <MrkSrvInd>N</MrkSrvInd> + </MrkSrv> + </Srv> + <Srv> + <GlobalProductCode>Y</GlobalProductCode> + <MrkSrv> + <LocalProductCode>Y</LocalProductCode> + <ProductShortName>EXPRESS 12:00</ProductShortName> + <LocalProductName>EXPRESS 12:00 NONDOC</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>N</POfferedCustAgreement> + <TransInd>Y</TransInd> + </MrkSrv> + <MrkSrv> + <LocalServiceType>FF</LocalServiceType> + <GlobalServiceName>FUEL SURCHARGE</GlobalServiceName> + <LocalServiceTypeName>FUEL SURCHARGE</LocalServiceTypeName> + <ChargeCodeType>SCH</ChargeCodeType> + <MrkSrvInd>N</MrkSrvInd> + </MrkSrv> + </Srv> + <Srv> + <GlobalProductCode>3</GlobalProductCode> + <MrkSrv> + <LocalProductCode>3</LocalProductCode> + <ProductShortName>B2C</ProductShortName> + <LocalProductName>EXPRESS WORLDWIDE (B2C)</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>Y</POfferedCustAgreement> + <TransInd>N</TransInd> + </MrkSrv> + </Srv> + <Srv> + <GlobalProductCode>P</GlobalProductCode> + <MrkSrv> + <LocalProductCode>P</LocalProductCode> + <ProductShortName>EXPRESS WORLDWIDE</ProductShortName> + <LocalProductName>EXPRESS WORLDWIDE NONDOC</LocalProductName> + <NetworkTypeCode>TD</NetworkTypeCode> + <POfferedCustAgreement>N</POfferedCustAgreement> + <TransInd>Y</TransInd> + </MrkSrv> + <MrkSrv> + <LocalServiceType>FF</LocalServiceType> + <GlobalServiceName>FUEL SURCHARGE</GlobalServiceName> + <LocalServiceTypeName>FUEL SURCHARGE</LocalServiceTypeName> + <ChargeCodeType>SCH</ChargeCodeType> + <MrkSrvInd>N</MrkSrvInd> + </MrkSrv> + </Srv> + </Srvs> + </GetQuoteResponse> +</res:DCTResponse> diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response_rates.php b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response_rates.php new file mode 100644 index 0000000000000..5646bb8464bf0 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response_rates.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 45.85, + 'method' => 'E', + ], + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 35.26, + 'method' => 'Q', + ], + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 37.38, + 'method' => 'Y', + ], + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 35.26, + 'method' => 'P', + ], +]; diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml new file mode 100644 index 0000000000000..e4d72bb94e78e --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.dhl.com ship-val-global-req-6.0.xsd" schemaVersion="6.0"> + <Request xmlns=""> + <ServiceHeader> + <MessageTime>currentTime</MessageTime> + <MessageReference>MAGE_SHIP_28TO32_Char_CHECKED</MessageReference> + <SiteID>some ID</SiteID> + <Password>some password</Password> + </ServiceHeader> + </Request> + <RegionCode xmlns="">CHECKED</RegionCode> + <RequestedPickupTime xmlns="">N</RequestedPickupTime> + <NewShipper xmlns="">N</NewShipper> + <LanguageCode xmlns="">EN</LanguageCode> + <PiecesEnabled xmlns="">Y</PiecesEnabled> + <Billing xmlns=""> + <ShipperAccountNumber>1234567890</ShipperAccountNumber> + <ShippingPaymentType>S</ShippingPaymentType> + <BillingAccountNumber>1234567890</BillingAccountNumber> + <DutyPaymentType>S</DutyPaymentType> + <DutyAccountNumber>1234567890</DutyAccountNumber> + </Billing> + <Consignee xmlns=""> + <CompanyName/> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact> + <PersonName/> + <PhoneNumber/> + </Contact> + </Consignee> + <Commodity xmlns=""> + <CommodityCode>1</CommodityCode> + </Commodity> + <Reference xmlns=""> + <ReferenceID>shipment reference</ReferenceID> + <ReferenceType>St</ReferenceType> + </Reference> + <ShipmentDetails xmlns=""> + <NumberOfPieces>1</NumberOfPieces> + <Pieces xmlns=""> + <Piece xmlns=""> + <PieceID>1</PieceID> + <PackageType>CP</PackageType> + <Weight>0.454</Weight> + <Width>3</Width> + <Height>3</Height> + <Depth>3</Depth> + <PieceContents>item_name</PieceContents> + </Piece> + </Pieces> + <Weight>0.454</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode/> + <LocalProductCode/> + <Date>currentTime</Date> + <Contents>DHL Parcel</Contents> + <DoorTo>DD</DoorTo> + <DimensionUnit>C</DimensionUnit> + <PackageType>CP</PackageType> + <CurrencyCode>USD</CurrencyCode> + </ShipmentDetails> + <Shipper xmlns=""> + <ShipperID>1234567890</ShipperID> + <CompanyName/> + <RegisteredAccount>1234567890</RegisteredAccount> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact xmlns=""> + <PersonName/> + <PhoneNumber/> + </Contact> + </Shipper> + <LabelImageFormat xmlns="">PDF</LabelImageFormat> +</req:ShipmentRequest> diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/euregion_dutiable_shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/euregion_dutiable_shipment_request.xml new file mode 100644 index 0000000000000..50a8c1b7a02a9 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/euregion_dutiable_shipment_request.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentValidateRequestEU xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com ship-val-req_EU.xsd"> + <Request xmlns=""> + <ServiceHeader> + <SiteID>some ID</SiteID> + <Password>some password</Password> + </ServiceHeader> + </Request> + <NewShipper xmlns="">N</NewShipper> + <LanguageCode xmlns="">EN</LanguageCode> + <PiecesEnabled xmlns="">Y</PiecesEnabled> + <Billing xmlns=""> + <ShipperAccountNumber>1234567890</ShipperAccountNumber> + <ShippingPaymentType>S</ShippingPaymentType> + <BillingAccountNumber>1234567890</BillingAccountNumber> + <DutyPaymentType>S</DutyPaymentType> + <DutyAccountNumber>1234567890</DutyAccountNumber> + </Billing> + <Consignee xmlns=""> + <CompanyName/> + <AddressLine/> + <City/> + <Division/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact> + <PersonName/> + <PhoneNumber/> + </Contact> + </Consignee> + <Commodity xmlns=""> + <CommodityCode>1</CommodityCode> + </Commodity> + <Dutiable> + <DeclaredValue>10.00</DeclaredValue> + <DeclaredCurrency>USD</DeclaredCurrency> + </Dutiable> + <Reference xmlns=""> + <ReferenceID>shipment reference</ReferenceID> + <ReferenceType>St</ReferenceType> + </Reference> + <ShipmentDetails xmlns=""> + <NumberOfPieces>1</NumberOfPieces> + <CurrencyCode>USD</CurrencyCode> + <Pieces xmlns=""> + <Piece xmlns=""> + <PieceID>1</PieceID> + <PackageType>CP</PackageType> + <Weight>0.5</Weight> + <Depth>3</Depth> + <Width>3</Width> + <Height>3</Height> + <PieceContents>item_name</PieceContents> + </Piece> + </Pieces> + <PackageType>CP</PackageType> + <Weight>0.454</Weight> + <DimensionUnit>C</DimensionUnit> + <WeightUnit>K</WeightUnit> + <GlobalProductCode/> + <LocalProductCode/> + <DoorTo>DD</DoorTo> + <Date/> + <Contents>DHL Parcel TEST</Contents> + </ShipmentDetails> + <Shipper xmlns=""> + <ShipperID>1234567890</ShipperID> + <CompanyName/> + <RegisteredAccount>1234567890</RegisteredAccount> + <AddressLine/> + <City/> + <Division/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact xmlns=""> + <PersonName/> + <PhoneNumber/> + </Contact> + </Shipper> + <LabelImageFormat xmlns="">PDF</LabelImageFormat> +</req:ShipmentValidateRequestEU> \ No newline at end of file diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/euregion_shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/euregion_shipment_request.xml new file mode 100644 index 0000000000000..6dccea89f8f6a --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/euregion_shipment_request.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentValidateRequestEU xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com ship-val-req_EU.xsd"> + <Request xmlns=""> + <ServiceHeader> + <SiteID>some ID</SiteID> + <Password>some password</Password> + </ServiceHeader> + </Request> + <NewShipper xmlns="">N</NewShipper> + <LanguageCode xmlns="">EN</LanguageCode> + <PiecesEnabled xmlns="">Y</PiecesEnabled> + <Billing xmlns=""> + <ShipperAccountNumber>1234567890</ShipperAccountNumber> + <ShippingPaymentType>S</ShippingPaymentType> + <BillingAccountNumber>1234567890</BillingAccountNumber> + <DutyPaymentType>S</DutyPaymentType> + <DutyAccountNumber>1234567890</DutyAccountNumber> + </Billing> + <Consignee xmlns=""> + <CompanyName/> + <AddressLine/> + <City/> + <Division/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact> + <PersonName/> + <PhoneNumber/> + </Contact> + </Consignee> + <Commodity xmlns=""> + <CommodityCode>1</CommodityCode> + </Commodity> + <Reference xmlns=""> + <ReferenceID>shipment reference</ReferenceID> + <ReferenceType>St</ReferenceType> + </Reference> + <ShipmentDetails xmlns=""> + <NumberOfPieces>1</NumberOfPieces> + <CurrencyCode>USD</CurrencyCode> + <Pieces xmlns=""> + <Piece xmlns=""> + <PieceID>1</PieceID> + <PackageType>CP</PackageType> + <Weight>0.5</Weight> + <Depth>3</Depth> + <Width>3</Width> + <Height>3</Height> + <PieceContents>item_name</PieceContents> + </Piece> + </Pieces> + <PackageType>CP</PackageType> + <Weight>0.454</Weight> + <DimensionUnit>C</DimensionUnit> + <WeightUnit>K</WeightUnit> + <GlobalProductCode/> + <LocalProductCode/> + <DoorTo>DD</DoorTo> + <Date/> + <Contents>DHL Parcel TEST</Contents> + </ShipmentDetails> + <Shipper xmlns=""> + <ShipperID>1234567890</ShipperID> + <CompanyName/> + <RegisteredAccount>1234567890</RegisteredAccount> + <AddressLine/> + <City/> + <Division/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact xmlns=""> + <PersonName/> + <PhoneNumber/> + </Contact> + </Shipper> + <LabelImageFormat xmlns="">PDF</LabelImageFormat> +</req:ShipmentValidateRequestEU> \ No newline at end of file diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/shipment_request.xml new file mode 100644 index 0000000000000..d411041c96072 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/shipment_request.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.dhl.com ship-val-global-req-6.0.xsd" schemaVersion="6.0"> + <Request xmlns=""> + <ServiceHeader> + <MessageTime>currentTime</MessageTime> + <MessageReference>MAGE_SHIP_28TO32_Char_CHECKED</MessageReference> + <SiteID>some ID</SiteID> + <Password>some password</Password> + </ServiceHeader> + </Request> + <RegionCode xmlns="">CHECKED</RegionCode> + <RequestedPickupTime xmlns="">N</RequestedPickupTime> + <NewShipper xmlns="">N</NewShipper> + <LanguageCode xmlns="">EN</LanguageCode> + <PiecesEnabled xmlns="">Y</PiecesEnabled> + <Billing xmlns=""> + <ShipperAccountNumber>1234567890</ShipperAccountNumber> + <ShippingPaymentType>S</ShippingPaymentType> + <BillingAccountNumber>1234567890</BillingAccountNumber> + <DutyPaymentType>S</DutyPaymentType> + <DutyAccountNumber>1234567890</DutyAccountNumber> + </Billing> + <Consignee xmlns=""> + <CompanyName/> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact> + <PersonName/> + <PhoneNumber/> + </Contact> + </Consignee> + <Commodity xmlns=""> + <CommodityCode>1</CommodityCode> + </Commodity> + <Dutiable xmlns=""> + <DeclaredValue>10.00</DeclaredValue> + <DeclaredCurrency>USD</DeclaredCurrency> + </Dutiable> + <Reference xmlns=""> + <ReferenceID>shipment reference</ReferenceID> + <ReferenceType>St</ReferenceType> + </Reference> + <ShipmentDetails xmlns=""> + <NumberOfPieces>1</NumberOfPieces> + <Pieces xmlns=""> + <Piece xmlns=""> + <PieceID>1</PieceID> + <PackageType>CP</PackageType> + <Weight>0.454</Weight> + <Width>3</Width> + <Height>3</Height> + <Depth>3</Depth> + <PieceContents>item_name</PieceContents> + </Piece> + </Pieces> + <Weight>0.454</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode/> + <LocalProductCode/> + <Date>currentTime</Date> + <Contents>DHL Parcel</Contents> + <DoorTo>DD</DoorTo> + <DimensionUnit>C</DimensionUnit> + <PackageType>CP</PackageType> + <IsDutiable>Y</IsDutiable> + <CurrencyCode>USD</CurrencyCode> + </ShipmentDetails> + <Shipper xmlns=""> + <ShipperID>1234567890</ShipperID> + <CompanyName/> + <RegisteredAccount>1234567890</RegisteredAccount> + <AddressLine/> + <City/> + <PostalCode/> + <CountryCode/> + <CountryName/> + <Contact xmlns=""> + <PersonName/> + <PhoneNumber/> + </Contact> + </Shipper> + <LabelImageFormat xmlns="">PDF</LabelImageFormat> +</req:ShipmentRequest> diff --git a/app/code/Magento/Dhl/etc/adminhtml/system.xml b/app/code/Magento/Dhl/etc/adminhtml/system.xml index 91ed6c6568a70..7ab37de2f3658 100644 --- a/app/code/Magento/Dhl/etc/adminhtml/system.xml +++ b/app/code/Magento/Dhl/etc/adminhtml/system.xml @@ -31,8 +31,9 @@ <field id="account" translate="label" type="text" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Account Number</label> </field> - <field id="content_type" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> - <label>Content Type</label> + <field id="content_type" translate="label comment" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Content Type (Non Domestic)</label> + <comment>Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic)</comment> <source_model>Magento\Dhl\Model\Source\Contenttype</source_model> </field> <field id="handling_type" translate="label" type="select" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> @@ -81,18 +82,12 @@ </depends> </field> <field id="doc_methods" translate="label" type="multiselect" sortOrder="170" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> - <label>Allowed Methods</label> + <label>Documents Allowed Methods</label> <source_model>Magento\Dhl\Model\Source\Method\Doc</source_model> - <depends> - <field id="content_type">D</field> - </depends> </field> <field id="nondoc_methods" translate="label" type="multiselect" sortOrder="170" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> - <label>Allowed Methods</label> + <label>Non Documents Allowed Methods</label> <source_model>Magento\Dhl\Model\Source\Method\Nondoc</source_model> - <depends> - <field id="content_type">N</field> - </depends> </field> <field id="ready_time" translate="label comment" type="text" sortOrder="180" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Ready time</label> diff --git a/app/code/Magento/Dhl/etc/countries.xml b/app/code/Magento/Dhl/etc/countries.xml index 48837dbefb576..792465ce45942 100644 --- a/app/code/Magento/Dhl/etc/countries.xml +++ b/app/code/Magento/Dhl/etc/countries.xml @@ -83,7 +83,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Austria</name> <domestic>1</domestic> </AT> @@ -132,7 +132,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Belgium</name> <domestic>1</domestic> </BE> @@ -146,7 +146,7 @@ <currency>BGN</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Bulgaria</name> <domestic>1</domestic> </BG> @@ -257,7 +257,7 @@ <currency>CHF</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Switzerland</name> </CH> <CI> @@ -331,7 +331,7 @@ <currency>CZK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Czech Republic, The</name> <domestic>1</domestic> </CZ> @@ -339,7 +339,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Germany</name> <domestic>1</domestic> </DE> @@ -353,7 +353,7 @@ <currency>DKK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Denmark</name> <domestic>1</domestic> </DK> @@ -389,7 +389,7 @@ <currency>EEK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Estonia</name> <domestic>1</domestic> </EE> @@ -410,7 +410,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Spain</name> <domestic>1</domestic> </ES> @@ -424,7 +424,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Finland</name> <domestic>1</domestic> </FI> @@ -457,7 +457,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>France</name> <domestic>1</domestic> </FR> @@ -471,7 +471,7 @@ <currency>GBP</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>United Kingdom</name> <domestic>1</domestic> </GB> @@ -549,7 +549,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Greece</name> <domestic>1</domestic> </GR> @@ -612,7 +612,7 @@ <currency>HUF</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Hungary</name> <domestic>1</domestic> </HU> @@ -633,7 +633,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Ireland, Republic Of</name> <domestic>1</domestic> </IE> @@ -668,14 +668,14 @@ <currency>ISK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Iceland</name> </IS> <IT> <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Italy</name> <domestic>1</domestic> </IT> @@ -834,7 +834,7 @@ <currency>LTL</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Lithuania</name> <domestic>1</domestic> </LT> @@ -842,7 +842,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Luxembourg</name> <domestic>1</domestic> </LU> @@ -850,7 +850,7 @@ <currency>LVL</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Latvia</name> <domestic>1</domestic> </LV> @@ -1039,7 +1039,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Netherlands, The</name> <domestic>1</domestic> </NL> @@ -1047,7 +1047,7 @@ <currency>NOK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Norway</name> </NO> <NP> @@ -1127,7 +1127,7 @@ <currency>PLN</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Poland</name> <domestic>1</domestic> </PL> @@ -1142,7 +1142,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Portugal</name> <domestic>1</domestic> </PT> @@ -1177,7 +1177,7 @@ <currency>RON</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Romania</name> <domestic>1</domestic> </RO> @@ -1231,7 +1231,7 @@ <currency>SEK</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Sweden</name> <domestic>1</domestic> </SE> @@ -1246,7 +1246,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Slovenia</name> <domestic>1</domestic> </SI> @@ -1254,7 +1254,7 @@ <currency>EUR</currency> <weight_unit>KG</weight_unit> <measure_unit>CM</measure_unit> - <region>EA</region> + <region>EU</region> <name>Slovakia</name> <domestic>1</domestic> </SK> diff --git a/app/code/Magento/Dhl/i18n/en_US.csv b/app/code/Magento/Dhl/i18n/en_US.csv index a5532c2cea963..0e4c7a8385b93 100644 --- a/app/code/Magento/Dhl/i18n/en_US.csv +++ b/app/code/Magento/Dhl/i18n/en_US.csv @@ -61,6 +61,7 @@ Title,Title Password,Password "Account Number","Account Number" "Content Type","Content Type" +"Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic)","Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic)" "Calculate Handling Fee","Calculate Handling Fee" "Handling Applied","Handling Applied" """Per Order"" allows a single handling fee for the entire order. ""Per Package"" allows an individual handling fee for each package.","""Per Order"" allows a single handling fee for the entire order. ""Per Package"" allows an individual handling fee for each package." diff --git a/app/code/Magento/Directory/Model/CurrencyConfig.php b/app/code/Magento/Directory/Model/CurrencyConfig.php index fdb561c224170..f7230df6e86ea 100644 --- a/app/code/Magento/Directory/Model/CurrencyConfig.php +++ b/app/code/Magento/Directory/Model/CurrencyConfig.php @@ -57,7 +57,7 @@ public function __construct( */ public function getConfigCurrencies(string $path) { - $result = $this->appState->getAreaCode() === Area::AREA_ADMINHTML + $result = in_array($this->appState->getAreaCode(), [Area::AREA_ADMINHTML, Area::AREA_CRONTAB]) ? $this->getConfigForAllStores($path) : $this->getConfigForCurrentStore($path); sort($result); diff --git a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php index 02fed61c44863..03e5d57232363 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php @@ -207,6 +207,7 @@ public function getItemById($countryId) /** * Add filter by country code to collection. + * * $countryCode can be either array of country codes or string representing one country code. * $iso can be either array containing 'iso2', 'iso3' values or string with containing one of that values directly. * The collection will contain countries where at least one of contry $iso fields matches $countryCode. @@ -299,7 +300,7 @@ public function toOptionArray($emptyLabel = ' ') } $options[] = $option; } - if ($emptyLabel !== false && count($options) > 0) { + if ($emptyLabel !== false && count($options) > 1) { array_unshift($options, ['value' => '', 'label' => $emptyLabel]); } diff --git a/app/code/Magento/Directory/Test/Mftf/Test/StorefrontCheckingCountryDropdownWithOneAllowedCountryTest.xml b/app/code/Magento/Directory/Test/Mftf/Test/StorefrontCheckingCountryDropdownWithOneAllowedCountryTest.xml new file mode 100644 index 0000000000000..90133737c6a1d --- /dev/null +++ b/app/code/Magento/Directory/Test/Mftf/Test/StorefrontCheckingCountryDropdownWithOneAllowedCountryTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckingCountryDropdownWithOneAllowedCountryTest"> + <annotations> + <features value="Directory"/> + <stories value="Country dropdown"/> + <title value="Checking country dropdown with one allowed country"/> + <description value="Checking country dropdown with one allowed country"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14810"/> + <useCaseId value="MAGETWO-76424"/> + <group value="directory"/> + <group value="customer"/> + </annotations> + <before> + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Set "Allow Countries" config to US only--> + <createData entity="SetAllowCountriesConfigUS" stepKey="setAllowCountriesConfigToUS"/> + <!--Flush cache--> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Login as Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$createCustomer$" /> + </actionGroup> + </before> + <after> + <!--Delete Customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Set "Allow Countries" config to default--> + <createData entity="DefaultAllowCountriesConfig" stepKey="setAllowCountriesConfigToDefault"/> + <!--Flush cache--> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Logout--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutFromStorefront"/> + </after> + + <!--Go to "Add New Address" page--> + <amOnPage url="{{StorefrontCustomerAddressNewPage.url}}" stepKey="goToAddNewAddressPage"/> + <!--Click on Country dropdown--> + <click selector="{{StorefrontCustomerAddressEditFormSection.country}}" stepKey="clickOnCountryDropdown"/> + <!--Check dropdown options--> + <see selector="{{StorefrontCustomerAddressEditFormSection.country}}" userInput="United States" stepKey="seeUSInCountryDropdown"/> + <dontSee selector="{{StorefrontCustomerAddressEditFormSection.country}}" userInput="Brazil" stepKey="dontSeeBrazilInCountryDropdown"/> + <dontSeeElement selector="{{StorefrontCustomerAddressEditFormSection.countryEmptyOption}}" stepKey="dontSeeEmptyOptionInCountryDropdown"/> + </test> +</tests> diff --git a/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php index 9b52bae26f90f..e594be90b26dd 100644 --- a/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php +++ b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php @@ -68,7 +68,7 @@ protected function setUp() } /** - * Test get currency config for admin and storefront areas. + * Test get currency config for admin, crontab and storefront areas. * * @dataProvider getConfigCurrenciesDataProvider * @return void @@ -91,7 +91,7 @@ public function testGetConfigCurrencies(string $areCode) ->method('getCode') ->willReturn('testCode'); - if ($areCode === Area::AREA_ADMINHTML) { + if (in_array($areCode, [Area::AREA_ADMINHTML, Area::AREA_CRONTAB])) { $this->storeManager->expects(self::once()) ->method('getStores') ->willReturn([$store]); @@ -121,6 +121,7 @@ public function getConfigCurrenciesDataProvider() { return [ ['areaCode' => Area::AREA_ADMINHTML], + ['areaCode' => Area::AREA_CRONTAB], ['areaCode' => Area::AREA_FRONTEND], ]; } diff --git a/app/code/Magento/Directory/composer.json b/app/code/Magento/Directory/composer.json index a4b5991095307..9681a951d9d06 100644 --- a/app/code/Magento/Directory/composer.json +++ b/app/code/Magento/Directory/composer.json @@ -10,7 +10,7 @@ "lib-libxml": "*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Downloadable/Model/SampleRepository.php b/app/code/Magento/Downloadable/Model/SampleRepository.php index 5b9e8e784b9f3..00f653e87b5ae 100644 --- a/app/code/Magento/Downloadable/Model/SampleRepository.php +++ b/app/code/Magento/Downloadable/Model/SampleRepository.php @@ -299,8 +299,11 @@ protected function updateSample( $existingSample->setTitle($sample->getTitle()); } - if ($sample->getSampleType() === 'file' && $sample->getSampleFileContent() === null) { - $sample->setSampleFile($existingSample->getSampleFile()); + if ($sample->getSampleType() === 'file' + && $sample->getSampleFileContent() === null + && $sample->getSampleFile() !== null + ) { + $existingSample->setSampleFile($sample->getSampleFile()); } $this->saveSample($product, $sample, $isGlobalScopeContent); return $existingSample->getId(); diff --git a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php index 64305cfce9b08..4aed7818b5095 100644 --- a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php +++ b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php @@ -92,9 +92,15 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($purchasedLink->getId()) { return $this; } + $storeId = $orderItem->getOrder()->getStoreId(); + $orderStatusToEnableItem = $this->_scopeConfig->getValue( + \Magento\Downloadable\Model\Link\Purchased\Item::XML_PATH_ORDER_ITEM_STATUS, + ScopeInterface::SCOPE_STORE, + $storeId + ); if (!$product) { $product = $this->_createProductModel()->setStoreId( - $orderItem->getOrder()->getStoreId() + $storeId )->load( $orderItem->getProductId() ); @@ -150,6 +156,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) )->setNumberOfDownloadsBought( $numberOfDownloads )->setStatus( + \Magento\Sales\Model\Order\Item::STATUS_PENDING == $orderStatusToEnableItem ? + \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_AVAILABLE : \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_PENDING )->setCreatedAt( $orderItem->getCreatedAt() diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php index a352c4bdf7bc3..9ab664cb27839 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php @@ -3,22 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Downloadable\Model\Product\Type; -use Magento\Downloadable\Model\Source\TypeUpload; use Magento\Downloadable\Model\Source\Shareable; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Downloadable\Model\Source\TypeUpload; use Magento\Framework\Stdlib\ArrayManager; -use Magento\Ui\Component\DynamicRows; use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Container; +use Magento\Ui\Component\DynamicRows; use Magento\Ui\Component\Form; /** - * Class adds a grid with links + * Class adds a grid with links. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Links extends AbstractModifier @@ -86,7 +88,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -101,7 +103,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -160,6 +162,8 @@ public function modifyMeta(array $meta) } /** + * Get dynamic rows meta. + * * @return array */ protected function getDynamicRows() @@ -180,6 +184,8 @@ protected function getDynamicRows() } /** + * Get single link record meta. + * * @return array */ protected function getRecord() @@ -221,6 +227,8 @@ protected function getRecord() } /** + * Get link title meta. + * * @return array */ protected function getTitleColumn() @@ -232,6 +240,7 @@ protected function getTitleColumn() 'label' => __('Title'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 10, ]; $titleField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, @@ -247,6 +256,8 @@ protected function getTitleColumn() } /** + * Get link price meta. + * * @return array */ protected function getPriceColumn() @@ -258,6 +269,7 @@ protected function getPriceColumn() 'label' => __('Price'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 20, ]; $priceField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, @@ -281,6 +293,8 @@ protected function getPriceColumn() } /** + * Get link file element meta. + * * @return array */ protected function getFileColumn() @@ -292,6 +306,7 @@ protected function getFileColumn() 'label' => __('File'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 30, ]; $fileTypeField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, @@ -344,6 +359,8 @@ protected function getFileColumn() } /** + * Get sample container meta. + * * @return array */ protected function getSampleColumn() @@ -355,6 +372,7 @@ protected function getSampleColumn() 'label' => __('Sample'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 40, ]; $sampleTypeField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, @@ -403,6 +421,8 @@ protected function getSampleColumn() } /** + * Get link "is sharable" element meta. + * * @return array */ protected function getShareableColumn() @@ -413,6 +433,7 @@ protected function getShareableColumn() 'componentType' => Form\Field::NAME, 'dataType' => Form\Element\DataType\Number::NAME, 'dataScope' => 'is_shareable', + 'sortOrder' => 50, 'options' => $this->shareable->toOptionArray(), ]; @@ -420,6 +441,8 @@ protected function getShareableColumn() } /** + * Get link "max downloads" element meta. + * * @return array */ protected function getMaxDownloadsColumn() @@ -431,6 +454,7 @@ protected function getMaxDownloadsColumn() 'label' => __('Max. Downloads'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 60, ]; $numberOfDownloadsField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php index 1587163ba8121..3890ee5b9e2b2 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php @@ -3,21 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Downloadable\Model\Product\Type; use Magento\Downloadable\Model\Source\TypeUpload; -use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Stdlib\ArrayManager; -use Magento\Ui\Component\DynamicRows; use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Container; +use Magento\Ui\Component\DynamicRows; use Magento\Ui\Component\Form; /** - * Class adds a grid with samples + * Class adds a grid with samples. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Samples extends AbstractModifier @@ -77,7 +79,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -90,7 +92,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -135,6 +137,8 @@ public function modifyMeta(array $meta) } /** + * Get sample rows meta. + * * @return array */ protected function getDynamicRows() @@ -155,6 +159,8 @@ protected function getDynamicRows() } /** + * Get single sample row meta. + * * @return array */ protected function getRecord() @@ -192,6 +198,8 @@ protected function getRecord() } /** + * Get sample title meta. + * * @return array */ protected function getTitleColumn() @@ -203,6 +211,7 @@ protected function getTitleColumn() 'showLabel' => false, 'label' => __('Title'), 'dataScope' => '', + 'sortOrder' => 10, ]; $titleField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, @@ -218,6 +227,8 @@ protected function getTitleColumn() } /** + * Get sample element meta. + * * @return array */ protected function getSampleColumn() @@ -229,6 +240,7 @@ protected function getSampleColumn() 'label' => __('File'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 20, ]; $sampleType['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, diff --git a/app/code/Magento/Downloadable/composer.json b/app/code/Magento/Downloadable/composer.json index e8a885cb49806..3ea7f69c2fad5 100644 --- a/app/code/Magento/Downloadable/composer.json +++ b/app/code/Magento/Downloadable/composer.json @@ -25,7 +25,7 @@ "magento/module-downloadable-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml index c86eb56a39008..79e93abf58b99 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml @@ -29,7 +29,7 @@ value="<?= /* @escapeNotVerified */ $_link->getId() ?>" <?= /* @escapeNotVerified */ $block->getLinkCheckedValue($_link) ?> price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_link->getPrice()) ?>"/> <?php endif; ?> - <label for="links_<?= /* @escapeNotVerified */ $_link->getId() ?>" class="label"> + <label for="links_<?= /* @escapeNotVerified */ $_link->getId() ?>" class="label admin__field-label"> <?= $block->escapeHtml($_link->getTitle()) ?> <?php if ($_link->getSampleFile() || $_link->getSampleUrl()): ?>  (<a href="<?= /* @escapeNotVerified */ $block->getLinkSampleUrl($_link) ?>" <?= $block->getIsOpenInNewWindow()?'onclick="this.target=\'_blank\'"':'' ?>><?= /* @escapeNotVerified */ __('sample') ?></a>) diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index 7f01cf268d06b..9b7abde37cc19 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -12,7 +12,6 @@ use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; -use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; use Magento\Framework\App\Config\Element; use Magento\Framework\DataObject; use Magento\Framework\DB\Adapter\DuplicateException; @@ -218,21 +217,12 @@ abstract class AbstractEntity extends AbstractResource implements EntityInterfac */ protected $objectRelationProcessor; - /** - * @var UniqueValidationInterface - */ - private $uniqueValidator; - /** * @param Context $context * @param array $data - * @param UniqueValidationInterface|null $uniqueValidator */ - public function __construct( - Context $context, - $data = [], - UniqueValidationInterface $uniqueValidator = null - ) { + public function __construct(Context $context, $data = []) + { $this->_eavConfig = $context->getEavConfig(); $this->_resource = $context->getResource(); $this->_attrSetEntity = $context->getAttributeSetEntity(); @@ -241,8 +231,6 @@ public function __construct( $this->_universalFactory = $context->getUniversalFactory(); $this->transactionManager = $context->getTransactionManager(); $this->objectRelationProcessor = $context->getObjectRelationProcessor(); - $this->uniqueValidator = $uniqueValidator ?: - ObjectManager::getInstance()->get(UniqueValidationInterface::class); parent::__construct(); $properties = get_object_vars($this); foreach ($data as $key => $value) { @@ -511,7 +499,6 @@ public function addAttributeByScope(AbstractAttribute $attribute, $entity = null /** * Get attributes by scope * - * @param string $suffix * @return array */ private function getAttributesByScope($suffix) @@ -982,8 +969,12 @@ public function checkAttributeUniqueValue(AbstractAttribute $attribute, $object) $data = $connection->fetchCol($select, $bind); - if ($object->getData($entityIdField)) { - return $this->uniqueValidator->validate($attribute, $object, $this, $entityIdField, $data); + $objectId = $object->getData($entityIdField); + if ($objectId) { + if (isset($data[0])) { + return $data[0] == $objectId; + } + return true; } return !count($data); @@ -1695,14 +1686,16 @@ public function saveAttribute(DataObject $object, $attributeCode) $connection->beginTransaction(); try { - $select = $connection->select()->from($table, 'value_id')->where($where); - $origValueId = $connection->fetchOne($select); + $select = $connection->select()->from($table, ['value_id', 'value'])->where($where); + $origRow = $connection->fetchRow($select); + $origValueId = $origRow['value_id'] ?? false; + $origValue = $origRow['value'] ?? null; if ($origValueId === false && $newValue !== null) { $this->_insertAttribute($object, $attribute, $newValue); } elseif ($origValueId !== false && $newValue !== null) { $this->_updateAttribute($object, $attribute, $origValueId, $newValue); - } elseif ($origValueId !== false && $newValue === null) { + } elseif ($origValueId !== false && $newValue === null && $origValue !== null) { $connection->delete($table, $where); } $this->_processAttributeValues(); @@ -1993,7 +1986,6 @@ public function afterDelete(DataObject $object) /** * Load attributes for object - * * If the object will not pass all attributes for this entity type will be loaded * * @param array $attributes diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Group.php b/app/code/Magento/Eav/Model/Entity/Attribute/Group.php index 0b6ac2b998de7..7c7fcec6fc8f8 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Group.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Group.php @@ -5,9 +5,14 @@ */ namespace Magento\Eav\Model\Entity\Attribute; +use Magento\Eav\Api\Data\AttributeGroupExtensionInterface; +use Magento\Eav\Api\Data\AttributeGroupInterface; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Model\AbstractExtensibleModel; /** + * Entity attribute group model. + * * @api * @method int getSortOrder() * @method \Magento\Eav\Model\Entity\Attribute\Group setSortOrder(int $value) @@ -19,14 +24,18 @@ * @method \Magento\Eav\Model\Entity\Attribute\Group setTabGroupCode(string $value) * @since 100.0.2 */ -class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements - \Magento\Eav\Api\Data\AttributeGroupInterface +class Group extends AbstractExtensibleModel implements AttributeGroupInterface { /** * @var \Magento\Framework\Filter\Translit */ private $translitFilter; + /** + * @var array + */ + private $reservedSystemNames = []; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -36,6 +45,7 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param array $reservedSystemNames */ public function __construct( \Magento\Framework\Model\Context $context, @@ -45,7 +55,8 @@ public function __construct( \Magento\Framework\Filter\Translit $translitFilter, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + array $reservedSystemNames = [] ) { parent::__construct( $context, @@ -57,10 +68,11 @@ public function __construct( $data ); $this->translitFilter = $translitFilter; + $this->reservedSystemNames = $reservedSystemNames; } /** - * Resource initialization + * Resource initialization. * * @return void * @codeCoverageIgnore @@ -71,7 +83,7 @@ protected function _construct() } /** - * Checks if current attribute group exists + * Checks if current attribute group exists. * * @return bool * @codeCoverageIgnore @@ -82,7 +94,7 @@ public function itemExists() } /** - * Delete groups + * Delete groups. * * @return $this * @codeCoverageIgnore @@ -93,7 +105,7 @@ public function deleteGroups() } /** - * Processing object before save data + * Processing object before save data. * * @return $this */ @@ -110,18 +122,20 @@ public function beforeSave() ), '-' ); - if (empty($attributeGroupCode)) { + $isReservedSystemName = in_array(strtolower($attributeGroupCode), $this->reservedSystemNames); + if (empty($attributeGroupCode) || $isReservedSystemName) { // in the following code md5 is not used for security purposes - $attributeGroupCode = md5($groupName); + $attributeGroupCode = md5(strtolower($groupName)); } $this->setAttributeGroupCode($attributeGroupCode); } } + return parent::beforeSave(); } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnoreStart */ public function getAttributeGroupId() @@ -130,7 +144,7 @@ public function getAttributeGroupId() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributeGroupName() { @@ -138,7 +152,7 @@ public function getAttributeGroupName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributeSetId() { @@ -146,7 +160,7 @@ public function getAttributeSetId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeGroupId($attributeGroupId) { @@ -154,7 +168,7 @@ public function setAttributeGroupId($attributeGroupId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeGroupName($attributeGroupName) { @@ -162,7 +176,7 @@ public function setAttributeGroupName($attributeGroupName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeSetId($attributeSetId) { @@ -170,9 +184,9 @@ public function setAttributeSetId($attributeSetId) } /** - * {@inheritdoc} + * @inheritdoc * - * @return \Magento\Eav\Api\Data\AttributeGroupExtensionInterface|null + * @return AttributeGroupExtensionInterface|null */ public function getExtensionAttributes() { @@ -180,14 +194,13 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * - * @param \Magento\Eav\Api\Data\AttributeGroupExtensionInterface $extensionAttributes + * @param AttributeGroupExtensionInterface $extensionAttributes * @return $this */ - public function setExtensionAttributes( - \Magento\Eav\Api\Data\AttributeGroupExtensionInterface $extensionAttributes - ) { + public function setExtensionAttributes(AttributeGroupExtensionInterface $extensionAttributes) + { return $this->_setExtensionAttributes($extensionAttributes); } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php index 0991b3f9f4b23..36ad026029056 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php @@ -73,13 +73,15 @@ public function getOptionText($value) } } // End - if (isset($options[$value])) { + if (is_scalar($value) && isset($options[$value])) { return $options[$value]; } return false; } /** + * Get option id. + * * @param string $value * @return null|string */ diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php deleted file mode 100644 index 50a6ff9329fc9..0000000000000 --- a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Eav\Model\Entity\Attribute; - -use Magento\Framework\DataObject; -use Magento\Eav\Model\Entity\AbstractEntity; - -/** - * Interface for unique attribute validator. - */ -interface UniqueValidationInterface -{ - /** - * Validate if attribute value is unique. - * - * @param AbstractAttribute $attribute - * @param DataObject $object - * @param AbstractEntity $entity - * @param string $entityLinkField - * @param array $entityIds - * @return bool - */ - public function validate( - AbstractAttribute $attribute, - DataObject $object, - AbstractEntity $entity, - string $entityLinkField, - array $entityIds - ): bool; -} diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php deleted file mode 100644 index 9aa501daf1584..0000000000000 --- a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Eav\Model\Entity\Attribute; - -use Magento\Framework\DataObject; -use Magento\Eav\Model\Entity\AbstractEntity; - -/** - * Class for validate unique attribute value. - */ -class UniqueValidator implements UniqueValidationInterface -{ - /** - * @inheritdoc - */ - public function validate( - AbstractAttribute $attribute, - DataObject $object, - AbstractEntity $entity, - string $entityLinkField, - array $entityIds - ): bool { - if (isset($entityIds[0])) { - return $entityIds[0] == $object->getData($entityLinkField); - } - - return true; - } -} diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 7ac77a66482b8..242a44d9dabfd 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -1035,6 +1035,7 @@ public function importFromArray($arr) $this->_items[$entityId]->addData($row); } } + $this->_setIsLoaded(); return $this; } diff --git a/app/code/Magento/Eav/Model/Form.php b/app/code/Magento/Eav/Model/Form.php index c8c50521f5509..a34b53eede354 100644 --- a/app/code/Magento/Eav/Model/Form.php +++ b/app/code/Magento/Eav/Model/Form.php @@ -286,7 +286,8 @@ public function getFormCode() } /** - * Return entity type instance + * Return entity type instance. + * * Return EAV entity type if entity type is not defined * * @return \Magento\Eav\Model\Entity\Type @@ -323,6 +324,8 @@ public function getAttributes() if ($this->_attributes === null) { $this->_attributes = []; $this->_userAttributes = []; + $this->_systemAttributes = []; + $this->_allowedAttributes = []; /** @var $attribute \Magento\Eav\Model\Attribute */ foreach ($this->_getFilteredFormAttributeCollection() as $attribute) { $this->_attributes[$attribute->getAttributeCode()] = $attribute; diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php index d4c91e98d9608..3f663558f4b8c 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php @@ -6,7 +6,12 @@ namespace Magento\Eav\Test\Unit\Model\Entity\Attribute; use Magento\Eav\Model\Entity\Attribute\Group; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group as ResourceGroup; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Filter\Translit; +use Magento\Framework\Model\Context; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class GroupTest extends \PHPUnit\Framework\TestCase { @@ -16,34 +21,38 @@ class GroupTest extends \PHPUnit\Framework\TestCase private $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ResourceGroup|MockObject */ private $resourceMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ManagerInterface|MockObject */ private $eventManagerMock; + /** + * @inheritdoc + */ protected function setUp() { - $this->resourceMock = $this->createMock(\Magento\Eav\Model\ResourceModel\Entity\Attribute\Group::class); - $translitFilter = $this->getMockBuilder(\Magento\Framework\Filter\Translit::class) + $this->resourceMock = $this->createMock(ResourceGroup::class); + $translitFilter = $this->getMockBuilder(Translit::class) ->disableOriginalConstructor() ->getMock(); $translitFilter->expects($this->atLeastOnce())->method('filter')->willReturnArgument(0); - $this->eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $contextMock = $this->createMock(\Magento\Framework\Model\Context::class); + $this->eventManagerMock = $this->createMock(ManagerInterface::class); + $contextMock = $this->createMock(Context::class); $contextMock->expects($this->any())->method('getEventDispatcher')->willReturn($this->eventManagerMock); $constructorArguments = [ 'resource' => $this->resourceMock, 'translitFilter' => $translitFilter, 'context' => $contextMock, + 'reservedSystemNames' => ['configurable'], ]; $objectManager = new ObjectManager($this); $this->model = $objectManager->getObject( - \Magento\Eav\Model\Entity\Attribute\Group::class, + Group::class, $constructorArguments ); } @@ -67,6 +76,8 @@ public function attributeGroupCodeDataProvider() { return [ ['General Group', 'general-group'], + ['configurable', md5('configurable')], + ['configurAble', md5('configurable')], ['///', md5('///')], ]; } diff --git a/app/code/Magento/Eav/composer.json b/app/code/Magento/Eav/composer.json index b1eec7fcaa602..b5f3c86947a0f 100644 --- a/app/code/Magento/Eav/composer.json +++ b/app/code/Magento/Eav/composer.json @@ -11,7 +11,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "101.0.6", + "version": "101.0.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index 92c1ef11b9c1f..c8afd10aa3eee 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Eav\Model\Entity\Setup\PropertyMapperInterface" type="Magento\Eav\Model\Entity\Setup\PropertyMapper\Composite" /> <preference for="Magento\Eav\Model\Entity\AttributeLoaderInterface" type="Magento\Eav\Model\Entity\AttributeLoader" /> - <preference for="Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface" type="Magento\Eav\Model\Entity\Attribute\UniqueValidator" /> <preference for="Magento\Eav\Api\Data\AttributeInterface" type="Magento\Eav\Model\Entity\Attribute" /> <preference for="Magento\Eav\Api\AttributeRepositoryInterface" type="Magento\Eav\Model\AttributeRepository" /> <preference for="Magento\Eav\Api\Data\AttributeGroupInterface" type="Magento\Eav\Model\Entity\Attribute\Group" /> diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Delete.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Delete.php index eedcf5009ccfa..a609a14f663c1 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Delete.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Delete.php @@ -12,9 +12,14 @@ class Delete extends \Magento\Email\Controller\Adminhtml\Email\Template * Delete transactional email action * * @return void + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $template = $this->_initTemplate('id'); if ($template->getId()) { try { diff --git a/app/code/Magento/Email/composer.json b/app/code/Magento/Email/composer.json index 482b63a2a34d7..6fda406b73483 100644 --- a/app/code/Magento/Email/composer.json +++ b/app/code/Magento/Email/composer.json @@ -15,7 +15,7 @@ "magento/module-theme": "100.2.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml index c1beb3ecb5a77..1bf0ea947605d 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -63,10 +63,11 @@ require([ "jquery", "tinymce", "Magento_Ui/js/modal/alert", + "mage/dataPost", "mage/mage", "Magento_Variable/variables", "prototype" -], function(jQuery, tinyMCE, alert){ +], function(jQuery, tinyMCE, alert, dataPost){ //<![CDATA[ jQuery('#email_template_edit_form').mage('form').mage('validation'); @@ -167,7 +168,10 @@ require([ deleteTemplate: function() { if(window.confirm("<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>")) { - window.location.href = '<?= $block->escapeJs($block->escapeUrl($block->getDeleteUrl())) ?>'; + dataPost().postData({ + action: '<?= $block->escapeJs($block->escapeUrl($block->getDeleteUrl())) ?>', + data: {} + }); } }, diff --git a/app/code/Magento/GiftMessage/composer.json b/app/code/Magento/GiftMessage/composer.json index d8fa39fa590a4..7ad147a15989a 100644 --- a/app/code/Magento/GiftMessage/composer.json +++ b/app/code/Magento/GiftMessage/composer.json @@ -17,7 +17,7 @@ "magento/module-multishipping": "100.2.*" }, "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/GoogleAnalytics/composer.json b/app/code/Magento/GoogleAnalytics/composer.json index 138aa560e8bcd..7c6a511f8b5e4 100644 --- a/app/code/Magento/GoogleAnalytics/composer.json +++ b/app/code/Magento/GoogleAnalytics/composer.json @@ -12,7 +12,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/GroupedImportExport/composer.json b/app/code/Magento/GroupedImportExport/composer.json index 092c40d011580..4e7573761f3a1 100644 --- a/app/code/Magento/GroupedImportExport/composer.json +++ b/app/code/Magento/GroupedImportExport/composer.json @@ -11,7 +11,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/GroupedProduct/composer.json b/app/code/Magento/GroupedProduct/composer.json index 7107903a91a58..508b424b2c022 100644 --- a/app/code/Magento/GroupedProduct/composer.json +++ b/app/code/Magento/GroupedProduct/composer.json @@ -21,7 +21,7 @@ "magento/module-grouped-product-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php index 6c99e59d1fe05..d51e75e2c753b 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php @@ -5,13 +5,10 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Import; +use Magento\Framework\Controller\ResultFactory; +use Magento\ImportExport\Block\Adminhtml\Import\Frame\Result; use Magento\ImportExport\Controller\Adminhtml\ImportResult as ImportResultController; use Magento\ImportExport\Model\Import; -use Magento\ImportExport\Block\Adminhtml\Import\Frame\Result; -use Magento\Framework\Controller\ResultFactory; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\ImportExport\Model\Import\Adapter as ImportAdapter; -use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; class Validate extends ImportResultController { diff --git a/app/code/Magento/ImportExport/composer.json b/app/code/Magento/ImportExport/composer.json index 7df4f98de95f4..9e9f1babad4a3 100644 --- a/app/code/Magento/ImportExport/composer.json +++ b/app/code/Magento/ImportExport/composer.json @@ -12,7 +12,7 @@ "ext-ctype": "*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php index b4a4d9f06ae48..dab47d091416e 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php @@ -6,18 +6,25 @@ */ namespace Magento\Indexer\Controller\Adminhtml\Indexer; +use Magento\Framework\Exception\NotFoundException; + class MassChangelog extends \Magento\Indexer\Controller\Adminhtml\Indexer { /** * Turn mview on for the given indexers * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $indexerIds = $this->getRequest()->getParam('indexer_ids'); if (!is_array($indexerIds)) { - $this->messageManager->addError(__('Please select indexers.')); + $this->messageManager->addErrorMessage(__('Please select indexers.')); } else { try { foreach ($indexerIds as $indexerId) { @@ -27,13 +34,13 @@ public function execute() )->get($indexerId); $model->setScheduled(true); } - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('%1 indexer(s) are in "Update by Schedule" mode.', count($indexerIds)) ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __("We couldn't change indexer(s)' mode because of an error.") ); diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php index 7ace4a64d3829..ac6bb046dba22 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php @@ -6,15 +6,22 @@ */ namespace Magento\Indexer\Controller\Adminhtml\Indexer; +use Magento\Framework\Exception\NotFoundException; + class MassOnTheFly extends \Magento\Indexer\Controller\Adminhtml\Indexer { /** * Turn mview off for the given indexers * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $indexerIds = $this->getRequest()->getParam('indexer_ids'); if (!is_array($indexerIds)) { $this->messageManager->addError(__('Please select indexers.')); diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 4e9b3dbdb8866..afeff3f38d63a 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -361,7 +361,7 @@ public function getLatestUpdated() return $this->getView()->getUpdated(); } } - return $this->getState()->getUpdated(); + return $this->getState()->getUpdated() ?: ''; } /** diff --git a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassChangelogTest.php b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassChangelogTest.php index df15a0a58949f..16b5f90cead58 100644 --- a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassChangelogTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassChangelogTest.php @@ -134,12 +134,10 @@ protected function setUp() \Magento\Framework\TestFramework\Unit\Helper\ObjectManager::class, ['get'] ); - $this->request = $this->getMockForAbstractClass( - \Magento\Framework\App\RequestInterface::class, - ['getParam', 'getRequest'], - '', - false - ); + $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['getParam', 'getRequest', 'isPost']) + ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->response->expects($this->any())->method("setRedirect")->willReturn(1); $this->page = $this->createMock(\Magento\Framework\View\Result\Page::class); @@ -147,7 +145,7 @@ protected function setUp() $this->title = $this->createMock(\Magento\Framework\View\Page\Title::class); $this->messageManager = $this->getMockForAbstractClass( \Magento\Framework\Message\ManagerInterface::class, - ['addError', 'addSuccess'], + ['addErrorMessage', 'addSuccessMessage'], '', false ); @@ -181,7 +179,7 @@ public function testExecute($indexerIds, $exception, $expectsExceptionValues) if (!is_array($indexerIds)) { $this->messageManager->expects($this->once()) - ->method('addError')->with(__('Please select indexers.')) + ->method('addErrorMessage')->with(__('Please select indexers.')) ->will($this->returnValue(1)); } else { $this->objectManager->expects($this->any()) @@ -210,10 +208,10 @@ public function testExecute($indexerIds, $exception, $expectsExceptionValues) if ($exception !== null) { $this->messageManager ->expects($this->exactly($expectsExceptionValues[2])) - ->method('addError') + ->method('addErrorMessage') ->with($exception->getMessage()); $this->messageManager->expects($this->exactly($expectsExceptionValues[1])) - ->method('addException') + ->method('addExceptionMessage') ->with($exception, "We couldn't change indexer(s)' mode because of an error."); } } diff --git a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php index a0e8134522461..956e102a3cc44 100644 --- a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php @@ -137,12 +137,9 @@ protected function setUp() \Magento\Framework\TestFramework\Unit\Helper\ObjectManager::class, ['get'] ); - $this->request = $this->getMockForAbstractClass( - \Magento\Framework\App\RequestInterface::class, - ['getParam', 'getRequest'], - '', - false - ); + $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['getParam', 'getRequest', 'isPost']) + ->getMockForAbstractClass(); $this->response->expects($this->any())->method("setRedirect")->willReturn(1); $this->page = $this->createMock(\Magento\Framework\View\Result\Page::class); @@ -181,6 +178,7 @@ public function testExecute($indexerIds, $exception, $expectsExceptionValues) $this->request->expects($this->any()) ->method('getParam')->with('indexer_ids') ->will($this->returnValue($indexerIds)); + $this->request->expects($this->any())->method('isPost')->willReturn(true); if (!is_array($indexerIds)) { $this->messageManager->expects($this->once()) diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index 6b7cc12218990..ca2da9585f934 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -164,7 +164,12 @@ public function testGetLatestUpdated($getViewIsEnabled, $getViewGetUpdated, $get } } } else { - $this->assertEquals($getStateGetUpdated, $this->model->getLatestUpdated()); + $getLatestUpdated = $this->model->getLatestUpdated(); + $this->assertEquals($getStateGetUpdated, $getLatestUpdated); + + if ($getStateGetUpdated === null) { + $this->assertNotNull($getLatestUpdated); + } } } @@ -182,7 +187,8 @@ public function getLatestUpdatedDataProvider() [true, '', '06-Jan-1944'], [true, '06-Jan-1944', ''], [true, '', ''], - [true, '06-Jan-1944', '05-Jan-1944'] + [true, '06-Jan-1944', '05-Jan-1944'], + [false, null, null], ]; } diff --git a/app/code/Magento/Indexer/composer.json b/app/code/Magento/Indexer/composer.json index a0fa62c3d7358..6b8ee0825fc14 100644 --- a/app/code/Magento/Indexer/composer.json +++ b/app/code/Magento/Indexer/composer.json @@ -7,7 +7,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/InstantPurchase/composer.json b/app/code/Magento/InstantPurchase/composer.json index d7257c7a46d90..1643692addbec 100644 --- a/app/code/Magento/InstantPurchase/composer.json +++ b/app/code/Magento/InstantPurchase/composer.json @@ -2,7 +2,7 @@ "name": "magento/module-instant-purchase", "description": "N/A", "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php index 36073af56327a..94f4c80239712 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php @@ -6,6 +6,7 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; +use Magento\Framework\Exception\NotFoundException; use Magento\Integration\Block\Adminhtml\Integration\Edit\Tab\Info; use Magento\Framework\Exception\IntegrationException; use Magento\Framework\Controller\ResultFactory; @@ -16,9 +17,14 @@ class Delete extends \Magento\Integration\Controller\Adminhtml\Integration * Delete the integration. * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $integrationId = (int)$this->getRequest()->getParam(self::PARAM_INTEGRATION_ID); diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php index dfddd8b954bd3..7afdfbb4de8c3 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php @@ -5,6 +5,7 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; +use Magento\Framework\Exception\NotFoundException; use Magento\Integration\Block\Adminhtml\Integration\Edit\Tab\Info; use Magento\Framework\Exception\IntegrationException; use Magento\Framework\Exception\LocalizedException; @@ -43,9 +44,15 @@ private function getSecurityCookie() * Save integration action. * * @return void + * @throws NotFoundException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + /** @var array $integrationData */ $integrationData = []; try { diff --git a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/DeleteTest.php b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/DeleteTest.php index 8173c50083973..1b2167ef104d1 100644 --- a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/DeleteTest.php +++ b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/DeleteTest.php @@ -23,6 +23,7 @@ protected function setUp() { parent::setUp(); + $this->_requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->integrationController = $this->_createIntegrationController('Delete'); $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) diff --git a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/SaveTest.php b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/SaveTest.php index ca614966194a2..7401b86d29d6b 100644 --- a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/SaveTest.php +++ b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/SaveTest.php @@ -17,6 +17,15 @@ class SaveTest extends \Magento\Integration\Test\Unit\Controller\Adminhtml\IntegrationTest { + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->_requestMock->expects($this->any())->method('isPost')->willReturn(true); + } + public function testSaveAction() { // Use real translate model diff --git a/app/code/Magento/Integration/composer.json b/app/code/Magento/Integration/composer.json index 74f2db3d68238..5d457d76cb94e 100644 --- a/app/code/Magento/Integration/composer.json +++ b/app/code/Magento/Integration/composer.json @@ -12,7 +12,7 @@ "magento/module-authorization": "100.2.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml b/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml index 8b7e787337e1a..95c9d15d72203 100644 --- a/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml +++ b/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml @@ -15,7 +15,8 @@ "jquery", 'Magento_Ui/js/modal/confirm', "jquery/ui", - "Magento_Integration/js/integration" + "Magento_Integration/js/integration", + 'mage/dataPost' ], function ($, Confirm) { window.integration = new Integration( @@ -37,7 +38,7 @@ content: "<?= /* @escapeNotVerified */ __("Are you sure you want to delete this integration? You can't undo this action.") ?>", actions: { confirm: function () { - window.location.href = $(e.target).data('url'); + $.mage.dataPost().postData({action: $(e.target).data('url'), data: {}}); } } }); @@ -47,4 +48,4 @@ }); </script> -<div id="integration-popup-container" style="display: none;"></div> \ No newline at end of file +<div id="integration-popup-container" style="display: none;"></div> diff --git a/app/code/Magento/LayeredNavigation/composer.json b/app/code/Magento/LayeredNavigation/composer.json index 8ce17fe30ec35..328549074f9b8 100644 --- a/app/code/Magento/LayeredNavigation/composer.json +++ b/app/code/Magento/LayeredNavigation/composer.json @@ -8,7 +8,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Msrp/Helper/Data.php b/app/code/Magento/Msrp/Helper/Data.php index b4ec34ebee19c..e8f8ca17a8b64 100644 --- a/app/code/Magento/Msrp/Helper/Data.php +++ b/app/code/Magento/Msrp/Helper/Data.php @@ -11,6 +11,8 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; /** * Msrp data helper @@ -70,8 +72,7 @@ public function __construct( } /** - * Check if can apply Minimum Advertise price to product - * in specific visibility + * Check if can apply MAP to product in specific visibility. * * @param int|Product $product * @param int|null $visibility Check displaying price in concrete place (by default generally) @@ -135,6 +136,8 @@ public function isShowPriceOnGesture($product) } /** + * Check if should show MAP price before order confirmation. + * * @param int|Product $product * @return bool */ @@ -144,6 +147,8 @@ public function isShowBeforeOrderConfirm($product) } /** + * Check if any MAP price is larger than "As low as" value. + * * @param int|Product $product * @return bool|float */ @@ -155,10 +160,18 @@ public function isMinimalPriceLessMsrp($product) $msrp = $product->getMsrp(); $price = $product->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE); if ($msrp === null) { - if ($product->getTypeId() !== \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) { - return false; - } else { + if ($product->getTypeId() === Grouped::TYPE_CODE) { $msrp = $product->getTypeInstance()->getChildrenMsrp($product); + } elseif ($product->getTypeId() === Configurable::TYPE_CODE) { + $prices = []; + foreach ($product->getTypeInstance()->getUsedProducts($product) as $item) { + if ($item->getMsrp() !== null) { + $prices[] = $item->getMsrp(); + } + } + $msrp = $prices ? max($prices) : 0; + } else { + return false; } } if ($msrp) { diff --git a/app/code/Magento/Msrp/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/AdminProductActionGroup.xml new file mode 100644 index 0000000000000..c8cdf8db42a7b --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminProductSetMsrp"> + <arguments> + <argument name="product"/> + <argument name="msrp" type="string" defaultValue="100"/> + </arguments> + <amOnPage url="{{AdminProductEditPage.url(product.id)}}" stepKey="goToProductEditPage"/> + <scrollToTopOfPage stepKey="scrollToTopToSeeAdvancedPricingButton"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitForMsrpElement"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.msrp}}" userInput="{{msrp}}" stepKey="fillMsrpField"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Msrp/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml new file mode 100644 index 0000000000000..b3742574c8235 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Check MSRP of product--> + <actionGroup name="AssertMsrpOfProduct"> + <arguments> + <argument name="msrp" type="string"/> + </arguments> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.msrp}}" stepKey="waitForMsrp"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.msrp}}" stepKey="grabMsrp"/> + <assertEquals stepKey="assertMsrp"> + <expectedResult type="string">${{msrp}}</expectedResult> + <actualResult type="variable">grabMsrp</actualResult> + </assertEquals> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="seeClickForPriceLink"/> + </actionGroup> + <!--Check product price when MSRP is not set or is less than price--> + <actionGroup name="AssertMsrpFallbackOfProduct" extends="AssertMsrpOfProduct"> + <remove keyForRemoval="seeClickForPriceLink"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.msrpFallback}}" stepKey="waitForMsrp"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.msrpFallback}}" stepKey="grabMsrp"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" after="assertMsrp" stepKey="dontSeeClickForPriceLink"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Msrp/Test/Mftf/Data/MsrpConfigData.xml b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpConfigData.xml new file mode 100644 index 0000000000000..731169aa40041 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpConfigData.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EnableMAPConfig" type="msrp_config"> + <requiredEntity type="enabled">EnableMAP</requiredEntity> + </entity> + <entity name="EnableMAP" type="msrp_config"> + <data key="value">1</data> + </entity> + + <entity name="DisableMAPConfig" type="msrp_config"> + <requiredEntity type="enabled">DisableMAP</requiredEntity> + </entity> + <entity name="DisableMAP" type="msrp_config"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_config-meta.xml b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_config-meta.xml new file mode 100644 index 0000000000000..f911d7072fb9a --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_config-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="MsrpConfig" dataType="msrp_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/sales/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="msrp_config"> + <object key="msrp" dataType="msrp_config"> + <object key="fields" dataType="msrp_config"> + <object key="enabled" dataType="enabled"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Msrp/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Msrp/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml new file mode 100644 index 0000000000000..08c0cb8d48309 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductFormAdvancedPricingSection"> + <element name="msrp" type="input" selector="input[name='product[msrp]']" timeout="5"/> + </section> +</sections> diff --git a/app/code/Magento/Msrp/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Msrp/Test/Mftf/Section/StorefrontProductInfoMainSection.xml new file mode 100644 index 0000000000000..7aba2d8d6e211 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductInfoMainSection"> + <element name="msrp" type="text" selector=".price-final_price .map-old-price .price-msrp_price .price-wrapper"/> + <element name="msrpFallback" type="text" selector=".price-final_price .map-fallback-price .price-msrp_price .price-wrapper"/> + <element name="clickForPriceLink" type="button" selector="//a[@class='action map-show-info' and contains(text(),'Click for price')]"/> + </section> +</sections> diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontCheckingConfigurableProductPriceWithMapTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontCheckingConfigurableProductPriceWithMapTest.xml new file mode 100644 index 0000000000000..1a2d4ad8e06e4 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontCheckingConfigurableProductPriceWithMapTest.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckingConfigurableProductPriceWithMapTest"> + <annotations> + <features value="Mrsp"/> + <stories value="Configurable child products with MAP"/> + <title value="Simple product with MAP assigned to configurable should displays the same way as products with special price"/> + <description value="Check that simple products with MAP assigned to configurable product displayed correctly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-15014"/> + <useCaseId value="MAGETWO-73985"/> + <group value="msrp"/> + <group value="configurableProduct"/> + </annotations> + <before> + <!--Enable MAP in configuration--> + <createData entity="EnableMAPConfig" stepKey="enableMapConfig"/> + <!--Create configurable product--> + <actionGroup ref="AdminCreateApiConfigurableProductWithThreeChildActionGroup" stepKey="createConfigProduct"/> + <!--Login--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Disable MAP in configuration--> + <createData entity="DisableMAPConfig" stepKey="disableMAPConfig"/> + <!--Delete entities--> + <deleteData createDataKey="createConfigProductCreateConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createChildProduct1CreateConfigProduct" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createChildProduct2CreateConfigProduct" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createChildProduct3CreateConfigProduct" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigProduct" stepKey="deleteConfigProductAttribute"/> + <!--Logout--> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Set MSRP for Child product 1--> + <actionGroup ref="AdminProductSetMsrp" stepKey="setMsrpForChildProduct1"> + <argument name="product" value="$createChildProduct1CreateConfigProduct$"/> + <argument name="msrp" value="45"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveChildProduct1"/> + + <!--Set MSRP for Child product 2--> + <actionGroup ref="AdminProductSetMsrp" stepKey="setMsrpForChildProduct2"> + <argument name="product" value="$createChildProduct2CreateConfigProduct$"/> + <argument name="msrp" value="66"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveChildProduct2"/> + + <amOnPage url="{{StorefrontProductPage.url($createConfigProductCreateConfigProduct.custom_attributes[url_key]$)}}" stepKey="goToProductPage"/> + <!--Checking MSRP of configurable product on Storefront page--> + <actionGroup ref="AssertMsrpOfProduct" stepKey="assertMsrpOfChildProduct"> + <argument name="msrp" value="66.00"/> + </actionGroup> + + <!--Checking when Option has a price higher than MSRP--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$getConfigAttributeOption1CreateConfigProduct.value$" stepKey="selectOption1"/> + <actionGroup ref="AssertMsrpFallbackOfProduct" stepKey="assertMsrpOfChildProduct1"> + <argument name="msrp" value="50.00"/> + </actionGroup> + + <!--Checking when Option has a price less than MSRP--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$getConfigAttributeOption2CreateConfigProduct.value$" stepKey="selectOption2"/> + <actionGroup ref="AssertMsrpOfProduct" stepKey="assertMsrpOfChildProduct2"> + <argument name="msrp" value="66.00"/> + </actionGroup> + + <!--Checking when Option doesn't have MSRP--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$getConfigAttributeOption3CreateConfigProduct.value$" stepKey="selectOption3"/> + <actionGroup ref="AssertMsrpFallbackOfProduct" stepKey="assertMsrpOfChildProduct3"> + <argument name="msrp" value="70.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Msrp/composer.json b/app/code/Magento/Msrp/composer.json index 625bec5d6c9d2..405c11dfe8195 100644 --- a/app/code/Magento/Msrp/composer.json +++ b/app/code/Magento/Msrp/composer.json @@ -8,6 +8,7 @@ "magento/module-downloadable": "100.2.*", "magento/module-eav": "101.0.*", "magento/module-grouped-product": "100.2.*", + "magento/module-configurable-product": "100.2.*", "magento/module-tax": "100.2.*", "magento/framework": "101.0.*" }, @@ -16,7 +17,7 @@ "magento/module-msrp-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml index 869d81563645a..ee282ebb82eb9 100644 --- a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml +++ b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml @@ -20,61 +20,79 @@ $priceType = $block->getPrice(); /** @var $product \Magento\Catalog\Model\Product */ $product = $block->getSaleableItem(); $productId = $product->getId(); +$amount = 0; + +if ($product->getMsrp()) { + $amount = $product->getMsrp(); +} elseif ($product->getTypeId() === \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) { + $amount = $product->getTypeInstance()->getChildrenMsrp($product); +} elseif ($product->getTypeId() === \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE) { + foreach ($product->getTypeInstance()->getUsedProducts($product) as $item) { + if ($item->getMsrp() !== null) { + $prices[] = $item->getMsrp(); + } + } + $amount = $prices ? max($prices) : 0; +} + $msrpPrice = $block->renderAmount( - $priceType->getCustomAmount($product->getMsrp() ?: $product->getTypeInstance()->getChildrenMsrp($product)), + $priceType->getCustomAmount($amount), [ 'price_id' => $block->getPriceId() ? $block->getPriceId() : 'old-price-' . $productId, 'include_container' => false, - 'skip_adjustments' => true + 'skip_adjustments' => true, ] ); $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElementIdPrefix() : 'product-price-'; - -$addToCartUrl = ''; -if ($product->isSaleable()) { - /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ - $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct'); - $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( - $product, - ['_query' => [ - \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => - $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl( - $addToCartUrlGenerator->getAddToCartUrl($product) - ), - ]] - ); -} ?> -<?php if ($product->getMsrp()): ?> + +<?php if ($amount): ?> <span class="old-price map-old-price"><?= /* @escapeNotVerified */ $msrpPrice ?></span> + <span class="map-fallback-price normal-price"><?= /* @escapeNotVerified */ $msrpPrice ?></span> <?php endif; ?> <?php if ($priceType->isShowPriceOnGesture()): ?> <?php - $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix(); - $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); - $data = ['addToCart' => [ - 'origin'=> 'msrp', - 'popupId' => '#' . $popupId, - 'productName' => $block->escapeJs($block->escapeHtml($product->getName())), - 'productId' => $productId, - 'productIdInput' => 'input[type="hidden"][name="product"]', - 'realPrice' => $block->getRealPriceHtml(), - 'isSaleable' => $product->isSaleable(), - 'msrpPrice' => $msrpPrice, - 'priceElementId' => $priceElementId, - 'closeButtonId' => '#map-popup-close', - 'addToCartUrl' => $addToCartUrl, - 'paymentButtons' => '[data-label=or]' - ]]; - if ($block->getRequest()->getFullActionName() === 'catalog_product_view') { - $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]'; - } else { - $data['addToCart']['addToCartButton'] = sprintf( - 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]', - (int) $productId - ); - } + + $addToCartUrl = ''; + if ($product->isSaleable()) { + /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ + $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct'); + $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( + $product, + ['_query' => [ + \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => + $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl( + $addToCartUrlGenerator->getAddToCartUrl($product) + ), + ]] + ); + } + + $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix(); + $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); + $data = ['addToCart' => [ + 'origin' => 'msrp', + 'popupId' => '#' . $popupId, + 'productName' => $block->escapeJs($block->escapeHtml($product->getName())), + 'productId' => $productId, + 'productIdInput' => 'input[type="hidden"][name="product"]', + 'realPrice' => $block->getRealPriceHtml(), + 'isSaleable' => $product->isSaleable(), + 'msrpPrice' => $msrpPrice, + 'priceElementId' => $priceElementId, + 'closeButtonId' => '#map-popup-close', + 'addToCartUrl' => $addToCartUrl, + 'paymentButtons' => '[data-label=or]' + ]]; + if ($block->getRequest()->getFullActionName() === 'catalog_product_view') { + $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]'; + } else { + $data['addToCart']['addToCartButton'] = sprintf( + 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]', + (int) $productId + ); + } ?> <span id="<?= /* @escapeNotVerified */ $block->getPriceId() ? $block->getPriceId() : $priceElementId ?>" style="display:none"></span> <a href="javascript:void(0);" diff --git a/app/code/Magento/Msrp/view/base/web/js/msrp.js b/app/code/Magento/Msrp/view/base/web/js/msrp.js index 72dd1d8bbecbe..d89c05f0b245f 100644 --- a/app/code/Magento/Msrp/view/base/web/js/msrp.js +++ b/app/code/Magento/Msrp/view/base/web/js/msrp.js @@ -4,11 +4,12 @@ */ define([ 'jquery', + 'Magento_Catalog/js/price-utils', 'underscore', 'jquery/ui', 'mage/dropdown', 'mage/template' -], function ($) { +], function ($, priceUtils, _) { 'use strict'; $.widget('mage.addToCart', { @@ -24,7 +25,14 @@ define([ // Selectors cartForm: '.form.map.checkout', msrpLabelId: '#map-popup-msrp', + msrpPriceElement: '#map-popup-msrp .price-wrapper', priceLabelId: '#map-popup-price', + priceElement: '#map-popup-price .price', + mapInfoLinks: '.map-show-info', + displayPriceElement: '.old-price.map-old-price .price-wrapper', + fallbackPriceElement: '.normal-price.map-fallback-price .price-wrapper', + displayPriceContainer: '.old-price.map-old-price', + fallbackPriceContainer: '.normal-price.map-fallback-price', popUpAttr: '[data-role=msrp-popup-template]', popupCartButtonId: '#map-popup-button', paypalCheckoutButons: '[data-action=checkout-form-submit]', @@ -59,9 +67,11 @@ define([ shadowHinter: 'popup popup-pointer' }, popupOpened: false, + wasOpened: false, /** - * Creates widget instance + * Creates widget instance. + * * @private */ _create: function () { @@ -73,11 +83,13 @@ define([ this.initTierPopup(); } $(this.options.cartButtonId).on('click', this._addToCartSubmit.bind(this)); + $(document).on('updateMsrpPriceBlock', this.onUpdateMsrpPrice.bind(this)); $(this.options.cartForm).on('submit', this._onSubmitForm.bind(this)); }, /** - * Init msrp popup + * Init msrp popup. + * * @private */ initMsrpPopup: function () { @@ -89,8 +101,7 @@ define([ $msrpPopup.trigger('contentUpdated'); $msrpPopup.find('button') - .on('click', - this.handleMsrpAddToCart.bind(this)) + .on('click', this.handleMsrpAddToCart.bind(this)) .filter(this.options.popupCartButtonId) .text($(this.options.addToCartButton).text()); @@ -104,7 +115,8 @@ define([ }, /** - * Init info popup + * Init info popup. + * * @private */ initInfoPopup: function () { @@ -123,7 +135,8 @@ define([ }, /** - * Init tier price popup + * Init tier price popup. + * * @private */ initTierPopup: function () { @@ -150,9 +163,9 @@ define([ }, /** - * handle 'AddToCart' click on Msrp popup - * @param {Object} ev + * Handle 'AddToCart' click on Msrp popup. * + * @param {Object} ev * @private */ handleMsrpAddToCart: function (ev) { @@ -165,7 +178,7 @@ define([ }, /** - * handle 'paypal checkout buttons' click on Msrp popup + * Handle 'paypal checkout buttons' click on Msrp popup. * * @private */ @@ -174,7 +187,7 @@ define([ }, /** - * handle 'AddToCart' click on Tier popup + * Handle 'AddToCart' click on Tier popup. * * @param {Object} ev * @private @@ -192,7 +205,7 @@ define([ }, /** - * handle 'paypal checkout buttons' click on Tier popup + * Handle 'paypal checkout buttons' click on Tier popup. * * @private */ @@ -205,7 +218,7 @@ define([ }, /** - * Open and set up popup + * Open and set up popup. * * @param {Object} event */ @@ -213,8 +226,12 @@ define([ var options = this.tierOptions || this.options; this.popUpOptions.position.of = $(event.target); - this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); - this.$popup.find(this.options.priceLabelId).html(options.realPrice); + + if (!this.wasOpened) { + this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); + this.$popup.find(this.options.priceLabelId).html(options.realPrice); + this.wasOpened = true; + } this.$popup.dropdownDialog(this.popUpOptions).dropdownDialog('open'); this._toggle(this.$popup); @@ -224,6 +241,7 @@ define([ }, /** + * Toggle MAP popup visibility. * * @param {HTMLElement} $elem * @private @@ -240,6 +258,7 @@ define([ }, /** + * Close MAP information popup. * * @param {HTMLElement} $elem */ @@ -249,7 +268,7 @@ define([ }, /** - * Handler for addToCart action + * Handler for addToCart action. * * @param {Object} e */ @@ -275,7 +294,91 @@ define([ }, /** - * Handler for submit form + * Call on event updatePrice. Proxy to updateMsrpPrice method. + * + * @param {Event} event + * @param {mixed} priceIndex + * @param {Object} prices + */ + onUpdateMsrpPrice: function onUpdateMsrpPrice(event, priceIndex, prices) { + + var defaultMsrp, + defaultPrice, + msrpPrice, + finalPrice; + + defaultMsrp = _.chain(prices).map(function (price) { + return price.msrpPrice.amount; + }).reject(function (p) { + return p === null; + }).max().value(); + + defaultPrice = _.chain(prices).map(function (p) { + return p.finalPrice.amount; + }).min().value(); + + if (typeof priceIndex !== 'undefined') { + msrpPrice = prices[priceIndex].msrpPrice.amount; + finalPrice = prices[priceIndex].finalPrice.amount; + + if (msrpPrice === null || msrpPrice <= finalPrice) { + this.updateNonMsrpPrice(priceUtils.formatPrice(finalPrice)); + } else { + this.updateMsrpPrice( + priceUtils.formatPrice(finalPrice), + priceUtils.formatPrice(msrpPrice), + false); + } + } else { + this.updateMsrpPrice( + priceUtils.formatPrice(defaultPrice), + priceUtils.formatPrice(defaultMsrp), + true); + } + }, + + /** + * Update prices for configurable product with MSRP enabled. + * + * @param {String} finalPrice + * @param {String} msrpPrice + * @param {Boolean} useDefaultPrice + */ + updateMsrpPrice: function (finalPrice, msrpPrice, useDefaultPrice) { + var options = this.tierOptions || this.options; + + $(this.options.fallbackPriceContainer).hide(); + $(this.options.displayPriceContainer).show(); + $(this.options.mapInfoLinks).show(); + + if (useDefaultPrice || !this.wasOpened) { + this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); + this.$popup.find(this.options.priceLabelId).html(options.realPrice); + $(this.options.displayPriceElement).html(msrpPrice); + this.wasOpened = true; + } + + if (!useDefaultPrice) { + this.$popup.find(this.options.msrpPriceElement).html(msrpPrice); + this.$popup.find(this.options.priceElement).html(finalPrice); + $(this.options.displayPriceElement).html(msrpPrice); + } + }, + + /** + * Display non MAP price for irrelevant products. + * + * @param {String} price + */ + updateNonMsrpPrice: function (price) { + $(this.options.fallbackPriceElement).html(price); + $(this.options.displayPriceContainer).hide(); + $(this.options.mapInfoLinks).hide(); + $(this.options.fallbackPriceContainer).show(); + }, + + /** + * Handler for submit form. * * @private */ diff --git a/app/code/Magento/Multishipping/composer.json b/app/code/Magento/Multishipping/composer.json index d32bb1f5bd82a..48095da856e02 100644 --- a/app/code/Magento/Multishipping/composer.json +++ b/app/code/Magento/Multishipping/composer.json @@ -15,7 +15,7 @@ "magento/module-theme": "100.2.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml index d8514ca77f9c2..4354cfb7c1c3e 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml @@ -55,6 +55,10 @@ <div class="box-content"> <address> <?= /* @noEscape */ $block->getCheckoutData()->getAddressHtml($block->getAddress()); ?> + <input type="hidden" + id="multishipping_billing_country_id" + value="<?= /* @noEscape */ $block->getAddress()->getCountryId(); ?>" + name="multishipping_billing_country_id"/> </address> </div> </div> @@ -79,36 +83,45 @@ if (isset($methodsForms[$code])) { $block->setMethodFormTemplate($code, $methodsForms[$code]); } - ?> - <dt class="item-title"> - <?php if ($methodsCount > 1) : ?> - <input type="radio" - id="p_method_<?= $block->escapeHtml($code); ?>" - value="<?= $block->escapeHtml($code); ?>" - name="payment[method]" - title="<?= $block->escapeHtml($_method->getTitle()) ?>" - <?php if ($checked) : ?> - checked="checked" + ?> + <div data-bind="scope: 'payment_method_<?= $block->escapeHtml($code);?>'"> + <dt class="item-title"> + <?php if ($methodsCount > 1) : ?> + <input type="radio" + id="p_method_<?= $block->escapeHtml($code); ?>" + value="<?= $block->escapeHtml($code); ?>" + name="payment[method]" + title="<?= $block->escapeHtml($_method->getTitle()) ?>" + data-bind=" + value: getCode(), + checked: isChecked, + click: selectPaymentMethod, + visible: isRadioButtonVisible()" + <?php if ($checked) : ?> + checked="checked" + <?php endif; ?> + class="radio"/> + <?php else : ?> + <input type="radio" + id="p_method_<?= $block->escapeHtml($code); ?>" + value="<?= $block->escapeHtml($code); ?>" + name="payment[method]" + data-bind=" + value: getCode(), + afterRender: selectPaymentMethod" + checked="checked" + class="radio solo method" /> <?php endif; ?> - class="radio"/> - <?php else : ?> - <input type="radio" - id="p_method_<?= $block->escapeHtml($code); ?>" - value="<?= $block->escapeHtml($code); ?>" - name="payment[method]" - checked="checked" - class="radio solo method" /> - <?php endif; ?> - <label for="p_method_<?= $block->escapeHtml($code); ?>"> - <?= $block->escapeHtml($_method->getTitle()) ?> - </label> - </dt> - <?php if ($html = $block->getChildHtml('payment.method.' . $code)) : ?> - <dd class="item-content <?= $checked ? '' : 'no-display'; ?>" - data-bind="scope: 'payment_method_<?= $block->escapeHtml($code);?>'"> - <?= /* @noEscape */ $html; ?> - </dd> - <?php endif; ?> + <label for="p_method_<?= $block->escapeHtml($code); ?>"> + <?= $block->escapeHtml($_method->getTitle()) ?> + </label> + </dt> + <?php if ($html = $block->getChildHtml('payment.method.' . $code)) : ?> + <dd class="item-content <?= $checked ? '' : 'no-display'; ?>"> + <?= /* @noEscape */ $html; ?> + </dd> + <?php endif; ?> + </div> <?php endforeach; ?> </dl> <?= $block->getChildHtml('payment_methods_after') ?> diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js index 9b867cd7217b1..3a6d73e304974 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js @@ -15,7 +15,7 @@ define([ opacity: 0.5, // CSS opacity for the 'Place Order' button when it's clicked and then disabled. pleaseWaitLoader: 'span.please-wait', // 'Submitting order information...' Ajax loader. placeOrderSubmit: 'button[type="submit"]', // The 'Place Order' button. - agreements: '#checkout-agreements' // Container for all of the checkout agreements and terms/conditions + agreements: '.checkout-agreements' // Container for all of the checkout agreements and terms/conditions }, /** diff --git a/app/code/Magento/NewRelicReporting/composer.json b/app/code/Magento/NewRelicReporting/composer.json index abb2a200ed723..b256b25716c87 100644 --- a/app/code/Magento/NewRelicReporting/composer.json +++ b/app/code/Magento/NewRelicReporting/composer.json @@ -13,7 +13,7 @@ "magento/magento-composer-installer": "*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php index 7293b350fcd01..0f2192dc442db 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php @@ -9,18 +9,25 @@ namespace Magento\Newsletter\Controller\Adminhtml\Queue; +use Magento\Framework\Exception\NotFoundException; + class Save extends \Magento\Newsletter\Controller\Adminhtml\Queue { /** - * Save Newsletter queue + * Save newsletter queue. * - * @throws \Magento\Framework\Exception\LocalizedException * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { try { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + /* @var $queue \Magento\Newsletter\Model\Queue */ $queue = $this->_objectManager->create(\Magento\Newsletter\Model\Queue::class); @@ -30,7 +37,9 @@ public function execute() $template = $this->_objectManager->create(\Magento\Newsletter\Model\Template::class)->load($templateId); if (!$template->getId() || $template->getIsSystem()) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please correct the newsletter template and try again.')); + throw new \Magento\Framework\Exception\LocalizedException( + __('Please correct the newsletter template and try again.') + ); } $queue->setTemplateId( diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php index 7f02e4ea13445..4794d86faa17a 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php @@ -6,11 +6,12 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Subscriber; -use Magento\Newsletter\Controller\Adminhtml\Subscriber; use Magento\Backend\App\Action\Context; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Framework\Exception\NotFoundException; +use Magento\Newsletter\Controller\Adminhtml\Subscriber; use Magento\Newsletter\Model\SubscriberFactory; -use Magento\Framework\App\ObjectManager; class MassDelete extends Subscriber { @@ -36,12 +37,17 @@ public function __construct( * Delete one or more subscribers action * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + $subscribersIds = $this->getRequest()->getParam('subscriber'); if (!is_array($subscribersIds)) { - $this->messageManager->addError(__('Please select one or more subscribers.')); + $this->messageManager->addErrorMessage(__('Please select one or more subscribers.')); } else { try { foreach ($subscribersIds as $subscriberId) { @@ -50,9 +56,11 @@ public function execute() ); $subscriber->delete(); } - $this->messageManager->addSuccess(__('Total of %1 record(s) were deleted.', count($subscribersIds))); + $this->messageManager->addSuccessMessage( + __('Total of %1 record(s) were deleted.', count($subscribersIds)) + ); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php index b61494f795905..3b3ea0d4c67a0 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php @@ -6,6 +6,7 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Subscriber; +use Magento\Framework\Exception\NotFoundException; use Magento\Newsletter\Controller\Adminhtml\Subscriber; use Magento\Backend\App\Action\Context; use Magento\Framework\App\Response\Http\FileFactory; @@ -37,9 +38,14 @@ public function __construct( * Unsubscribe one or more subscribers action * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + $subscribersIds = $this->getRequest()->getParam('subscriber'); if (!is_array($subscribersIds)) { $this->messageManager->addError(__('Please select one or more subscribers.')); diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php index d327d44feceb8..ac47f7e217b36 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -12,9 +11,14 @@ class Delete extends \Magento\Newsletter\Controller\Adminhtml\Template * Delete newsletter Template * * @return void + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $template = $this->_objectManager->create( \Magento\Newsletter\Model\Template::class )->load( diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php index 52d46065ad05b..54a5eb651d99b 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php @@ -6,15 +6,22 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Template; +use Magento\Framework\Exception\NotFoundException; + class Drop extends \Magento\Newsletter\Controller\Adminhtml\Template { /** * Drop Newsletter Template * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $this->_view->loadLayout('newsletter_template_preview_popup'); $this->_view->renderLayout(); } diff --git a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php index 792dcf2fbe689..58b51009c205a 100644 --- a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php +++ b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php @@ -6,13 +6,12 @@ namespace Magento\Newsletter\Model\Plugin; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; -use Magento\Customer\Api\Data\CustomerExtensionInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Newsletter\Model\SubscriberFactory; use Magento\Framework\Api\ExtensionAttributesFactory; -use Magento\Framework\App\ObjectManager; use Magento\Newsletter\Model\ResourceModel\Subscriber; -use Magento\Newsletter\Model\SubscriberFactory; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Customer\Api\Data\CustomerExtensionInterface; +use Magento\Framework\App\ObjectManager; class CustomerPlugin { @@ -38,30 +37,22 @@ class CustomerPlugin */ private $customerSubscriptionStatus = []; - /** - * @var StoreManagerInterface - */ - private $storeManager; - /** * Initialize dependencies. * * @param SubscriberFactory $subscriberFactory * @param ExtensionAttributesFactory|null $extensionFactory * @param Subscriber|null $subscriberResource - * @param StoreManagerInterface|null $storeManager */ public function __construct( SubscriberFactory $subscriberFactory, ExtensionAttributesFactory $extensionFactory = null, - Subscriber $subscriberResource = null, - StoreManagerInterface $storeManager = null + Subscriber $subscriberResource = null ) { $this->subscriberFactory = $subscriberFactory; $this->extensionFactory = $extensionFactory ?: ObjectManager::getInstance()->get(ExtensionAttributesFactory::class); $this->subscriberResource = $subscriberResource ?: ObjectManager::getInstance()->get(Subscriber::class); - $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -158,8 +149,6 @@ public function afterDelete(CustomerRepository $subject, $result, CustomerInterf public function afterGetById(CustomerRepository $subject, CustomerInterface $customer) { $extensionAttributes = $customer->getExtensionAttributes(); - $storeId = $this->storeManager->getStore()->getId(); - $customer->setStoreId($storeId); if ($extensionAttributes === null) { /** @var CustomerExtensionInterface $extensionAttributes */ $extensionAttributes = $this->extensionFactory->create(CustomerInterface::class); diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php index 0bc79244bdf1c..39a9c2a0d95d2 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php @@ -10,8 +10,6 @@ use Magento\Customer\Api\Data\CustomerExtensionInterface; use Magento\Framework\Api\ExtensionAttributesFactory; use Magento\Newsletter\Model\ResourceModel\Subscriber; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; class CustomerPluginTest extends \PHPUnit\Framework\TestCase { @@ -55,11 +53,6 @@ class CustomerPluginTest extends \PHPUnit\Framework\TestCase */ private $customerMock; - /** - * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $storeManagerMock; - protected function setUp() { $this->subscriberFactory = $this->getMockBuilder(\Magento\Newsletter\Model\SubscriberFactory::class) @@ -94,8 +87,6 @@ protected function setUp() ->setMethods(["getExtensionAttributes"]) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); - $this->subscriberFactory->expects($this->any())->method('create')->willReturn($this->subscriber); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->plugin = $this->objectManager->getObject( @@ -103,8 +94,7 @@ protected function setUp() [ 'subscriberFactory' => $this->subscriberFactory, 'extensionFactory' => $this->extensionFactoryMock, - 'subscriberResource' => $this->subscriberResourceMock, - 'storeManager' => $this->storeManagerMock, + 'subscriberResource' => $this->subscriberResourceMock ] ); } @@ -208,7 +198,6 @@ public function testAfterGetByIdCreatesExtensionAttributesIfItIsNotSet( ) { $subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); $subscriber = [$subscriberStatusKey => $subscriberStatusValue]; - $this->prepareStoreData(); $this->extensionFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->customerExtensionMock); @@ -234,7 +223,6 @@ public function testAfterGetByIdSetsIsSubscribedFlagIfItIsNotSet() { $subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); $subscriber = ['subscriber_id' => 1, 'subscriber_status' => 1]; - $this->prepareStoreData(); $this->customerMock->expects($this->any()) ->method('getExtensionAttributes') ->willReturn($this->customerExtensionMock); @@ -267,17 +255,4 @@ public function afterGetByIdDataProvider() [null, null, false] ]; } - - /** - * Prepare store information - * - * @return void - */ - private function prepareStoreData() - { - $storeId = 1; - $storeMock = $this->createMock(Store::class); - $storeMock->expects($this->any())->method('getId')->willReturn($storeId); - $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); - } } diff --git a/app/code/Magento/Newsletter/composer.json b/app/code/Magento/Newsletter/composer.json index a97d0bca5634d..9e02676e2488c 100644 --- a/app/code/Magento/Newsletter/composer.json +++ b/app/code/Magento/Newsletter/composer.json @@ -14,7 +14,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml index eeca4fabd348d..b2a1c8d8c7208 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml @@ -34,9 +34,10 @@ require([ 'tinymce', 'Magento_Ui/js/modal/prompt', 'Magento_Ui/js/modal/confirm', + 'mage/dataPost', 'mage/mage', 'prototype' -], function(jQuery, tinyMCE, prompt, confirm){ +], function(jQuery, tinyMCE, prompt, confirm, dataPost){ //<![CDATA[ jQuery('#newsletter_template_edit_form').mage('form').mage('validation'); @@ -203,7 +204,10 @@ require([ content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>", actions: { confirm: function() { - window.location.href = '<?= $block->escapeUrl($block->getDeleteUrl()) ?>'; + dataPost().postData({ + action: '<?= $block->escapeUrl($block->getDeleteUrl()) ?>', + data: {} + }); } } }); diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml new file mode 100644 index 0000000000000..4d63577319d5b --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +/** + * @var $block \Magento\OfflinePayments\Block\Info\Checkmo + */ +?> +<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> + {{pdf_row_separator}} +<?php if ($block->getInfo()->getAdditionalInformation()): ?> + {{pdf_row_separator}} + <?php if ($block->getPayableTo()): ?> + <?= $block->escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?> + {{pdf_row_separator}} + <?php endif; ?> + <?php if ($block->getMailingAddress()): ?> + <?= $block->escapeHtml(__('Send Check to:')) ?> + {{pdf_row_separator}} + <?= /* @noEscape */ nl2br($block->escapeHtml($block->getMailingAddress())) ?> + {{pdf_row_separator}} + <?php endif; ?> +<?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml new file mode 100644 index 0000000000000..4a6ea1c00b21c --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** + * @var $block \Magento\OfflinePayments\Block\Info\Purchaseorder + */ +?> +<?= $block->escapeHtml(__('Purchase Order Number: %1', $block->getInfo()->getPoNumber())) ?> + {{pdf_row_separator}} diff --git a/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml new file mode 100644 index 0000000000000..32810ecef20da --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="checkout_billing"> + <arguments> + <argument name="form_templates" xsi:type="array"> + <item name="checkmo" xsi:type="string">Magento_OfflinePayments::multishipping/checkmo_form.phtml</item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml new file mode 100644 index 0000000000000..b96918243a7a7 --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<script> + require([ + 'uiLayout', + 'jquery' + ], function (layout, $) { + $(function () { + var paymentMethodData = { + method: 'checkmo' + }; + layout([ + { + component: 'Magento_Checkout/js/view/payment/default', + name: 'payment_method_checkmo', + method: paymentMethodData.method, + item: paymentMethodData + } + ]); + + $('body').trigger('contentUpdated'); + }) + }) +</script> diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php index 2373b5285ed00..0fc56c7136327 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php @@ -80,9 +80,8 @@ public function collectRates(RateRequest $request) $this->_updateFreeMethodQuote($request); - if ($request->getFreeShipping() || $request->getBaseSubtotalInclTax() >= $this->getConfigData( - 'free_shipping_subtotal' - ) + if ($request->getFreeShipping() + || ($request->getPackageValueWithDiscount() >= $this->getConfigData('free_shipping_subtotal')) ) { /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */ $method = $this->_rateMethodFactory->create(); diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php index 8d75cc32914b4..635d0c636ca36 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php @@ -13,6 +13,11 @@ class ImportTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import */ @@ -29,11 +34,16 @@ protected function setUp() \Magento\Framework\Data\Form::class, ['getFieldNameSuffix', 'addSuffixToName', 'getHtmlIdPrefix', 'getHtmlIdSuffix'] ); + $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->escaperMock->method('escapeHtml')->willReturnArgument(0); $testData = ['name' => 'test_name', 'html_id' => 'test_html_id']; $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_object = $testHelper->getObject( \Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import::class, - ['data' => $testData] + [ + 'escaper' => $this->escaperMock, + 'data' => $testData, + ] ); $this->_object->setForm($this->_formMock); } diff --git a/app/code/Magento/OfflineShipping/composer.json b/app/code/Magento/OfflineShipping/composer.json index e04816a3b1128..416181733dc07 100644 --- a/app/code/Magento/OfflineShipping/composer.json +++ b/app/code/Magento/OfflineShipping/composer.json @@ -19,7 +19,7 @@ "magento/module-offline-shipping-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/PageCache/composer.json b/app/code/Magento/PageCache/composer.json index 2b6b62aef2c47..e83801344dae7 100644 --- a/app/code/Magento/PageCache/composer.json +++ b/app/code/Magento/PageCache/composer.json @@ -9,7 +9,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl index 793f8f81a03f9..21f48ef76502f 100644 --- a/app/code/Magento/PageCache/etc/varnish4.vcl +++ b/app/code/Magento/PageCache/etc/varnish4.vcl @@ -91,10 +91,11 @@ sub vcl_recv { } } - # Remove Google gclid parameters to minimize the cache objects - set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA" - set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar" - set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz" + # Remove all marketing get parameters to minimize the cache objects + if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") { + set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", ""); + set req.url = regsub(req.url, "[?|&]+$", ""); + } # Static files caching if (req.url ~ "^/(pub/)?(media|static)/") { diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl index 4dce6356d1e73..23df172dd3aa4 100644 --- a/app/code/Magento/PageCache/etc/varnish5.vcl +++ b/app/code/Magento/PageCache/etc/varnish5.vcl @@ -92,10 +92,11 @@ sub vcl_recv { } } - # Remove Google gclid parameters to minimize the cache objects - set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA" - set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar" - set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz" + # Remove all marketing get parameters to minimize the cache objects + if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") { + set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", ""); + set req.url = regsub(req.url, "[?|&]+$", ""); + } # Static files caching if (req.url ~ "^/(pub/)?(media|static)/") { diff --git a/app/code/Magento/Payment/composer.json b/app/code/Magento/Payment/composer.json index b5e7ecdfdaff9..2f00b878417bd 100644 --- a/app/code/Magento/Payment/composer.json +++ b/app/code/Magento/Payment/composer.json @@ -12,7 +12,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml b/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml new file mode 100644 index 0000000000000..7acac62f65d38 --- /dev/null +++ b/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +/** + * @see \Magento\Payment\Block\Info + * @var \Magento\Payment\Block\Info $block + */ +?> +<?= $block->escapeHtml($block->getMethod()->getTitle()) ?>{{pdf_row_separator}} + +<?php if ($specificInfo = $block->getSpecificInformation()):?> + <?php foreach ($specificInfo as $label => $value):?> + <?= $block->escapeHtml($label) ?>: + <?= $block->escapeHtml(implode(' ', $block->getValueAsArray($value))) ?> + {{pdf_row_separator}} + <?php endforeach; ?> +<?php endif;?> + +<?= $block->escapeHtml(implode('{{pdf_row_separator}}', $block->getChildPdfAsArray())) ?> diff --git a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php index 497e32157de05..fc257e264d680 100644 --- a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php +++ b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php @@ -6,9 +6,11 @@ namespace Magento\Paypal\Controller\Transparent; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\Session\Generic; use Magento\Framework\Session\SessionManager; use Magento\Framework\Session\SessionManagerInterface; @@ -49,6 +51,11 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action */ private $transparent; + /** + * @var Validator + */ + private $formKeyValidator; + /** * @param Context $context * @param JsonFactory $resultJsonFactory @@ -57,6 +64,7 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action * @param SessionManager $sessionManager * @param Transparent $transparent * @param SessionManagerInterface|null $sessionInterface + * @param Validator $formKeyValidator */ public function __construct( Context $context, @@ -65,13 +73,16 @@ public function __construct( SecureToken $secureTokenService, SessionManager $sessionManager, Transparent $transparent, - SessionManagerInterface $sessionInterface = null + SessionManagerInterface $sessionInterface = null, + Validator $formKeyValidator = null ) { $this->resultJsonFactory = $resultJsonFactory; $this->sessionTransparent = $sessionTransparent; $this->secureTokenService = $secureTokenService; $this->sessionManager = $sessionInterface ?: $sessionManager; $this->transparent = $transparent; + $this->formKeyValidator = $formKeyValidator ?: ObjectManager::getInstance()->get(Validator::class); + parent::__construct($context); } @@ -85,8 +96,9 @@ public function execute() /** @var Quote $quote */ $quote = $this->sessionManager->getQuote(); - if (!$quote || !$quote instanceof Quote) { - return $this->getErrorResponse(); + if (!$quote || !$quote instanceof Quote || !$this->formKeyValidator->validate($this->getRequest()) + || !$this->getRequest()->isPost()) { + return $this->getErrorResponse(); } $this->sessionTransparent->setQuoteId($quote->getId()); diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index f0b86588f1cfa..69e821910d84c 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -1070,6 +1070,7 @@ protected static function cmpShippingOptions(DataObject $option1, DataObject $op */ protected function _matchShippingMethodCode(Address $address, $selectedCode): string { + $address->collectShippingRates(); $options = $this->_prepareShippingOptions($address, false); foreach ($options as $option) { if ($selectedCode === $option['code'] // the proper case as outlined in documentation diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php index 06a8a5b680bf4..259f00ec5a9c5 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php +++ b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Paypal\Model\Payflow\Service\Response; use Magento\Framework\DataObject; +use Magento\Framework\Intl\DateTimeFactory; use Magento\Payment\Model\Method\Logger; use Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface; use Magento\Framework\Session\Generic; @@ -18,6 +21,8 @@ /** * Class Transaction + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Transaction { @@ -51,6 +56,11 @@ class Transaction */ private $logger; + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + /** * @param Generic $sessionTransparent * @param CartRepositoryInterface $quoteRepository @@ -58,6 +68,7 @@ class Transaction * @param PaymentMethodManagementInterface $paymentManagement * @param HandlerInterface $errorHandler * @param Logger $logger + * @param DateTimeFactory $dateTimeFactory */ public function __construct( Generic $sessionTransparent, @@ -65,7 +76,8 @@ public function __construct( Transparent $transparent, PaymentMethodManagementInterface $paymentManagement, HandlerInterface $errorHandler, - Logger $logger + Logger $logger, + DateTimeFactory $dateTimeFactory ) { $this->sessionTransparent = $sessionTransparent; $this->quoteRepository = $quoteRepository; @@ -73,6 +85,7 @@ public function __construct( $this->paymentManagement = $paymentManagement; $this->errorHandler = $errorHandler; $this->logger = $logger; + $this->dateTimeFactory = $dateTimeFactory; } /** @@ -114,8 +127,45 @@ public function savePaymentInQuote($response) $payment->setData(OrderPaymentInterface::CC_TYPE, $response->getData(OrderPaymentInterface::CC_TYPE)); $payment->setAdditionalInformation(Payflowpro::PNREF, $response->getData(Payflowpro::PNREF)); + $expDate = $response->getData('expdate'); + $expMonth = $this->getCcExpMonth($expDate); + $payment->setCcExpMonth($expMonth); + $expYear = $this->getCcExpYear($expDate); + $payment->setCcExpYear($expYear); + $this->errorHandler->handle($payment, $response); $this->paymentManagement->set($quote->getId(), $payment); } + + /** + * Extracts expiration month from PayPal response expiration date. + * + * @param string $expDate format {MMYY} + * @return int + */ + private function getCcExpMonth(string $expDate): int + { + return (int)substr($expDate, 0, 2); + } + + /** + * Extracts expiration year from PayPal response expiration date. + * + * @param string $expDate format {MMYY} + * @return int + */ + private function getCcExpYear(string $expDate): int + { + $last2YearDigits = (int)substr($expDate, 2, 2); + $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + $first2YearDigits = (int)substr($currentDate->format('Y'), 0, 2); + + // case when credit card expires at next century + if ((int)$currentDate->format('y') > $last2YearDigits) { + $first2YearDigits++; + } + + return 100 * $first2YearDigits + $last2YearDigits; + } } diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php index b33d2f5723961..dd113562783aa 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php @@ -37,6 +37,7 @@ protected function setUp() ->setMethods( [ 'getHtmlId', + 'getName', 'getTooltip', 'getForm', ] diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php index 60451a9827097..c404ef54aad1d 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php @@ -6,13 +6,15 @@ namespace Magento\Paypal\Test\Unit\Controller\Transparent; use Magento\Framework\App\Action\Context; +use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\Session\Generic; use Magento\Framework\Session\SessionManager; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Paypal\Controller\Transparent\RequestSecureToken; use Magento\Paypal\Model\Payflow\Service\Request\SecureToken; use Magento\Paypal\Model\Payflow\Transparent; +use Magento\Quote\Model\Quote; /** * Class RequestSecureTokenTest @@ -56,6 +58,11 @@ class RequestSecureTokenTest extends \PHPUnit\Framework\TestCase */ protected $sessionManagerMock; + /** + * @var Validator|\PHPUnit_Framework_MockObject_MockObject + */ + private $formKeyValidator; + /** * Set up * @@ -64,9 +71,16 @@ class RequestSecureTokenTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $request->expects($this->any())->method('isPost')->willReturn(true); $this->contextMock = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class) ->disableOriginalConstructor() ->getMock(); + $this->contextMock->method('getRequest') + ->willReturn($request); + $this->resultJsonFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() @@ -90,13 +104,19 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->formKeyValidator = $this->getMockBuilder(Validator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->controller = new \Magento\Paypal\Controller\Transparent\RequestSecureToken( $this->contextMock, $this->resultJsonFactoryMock, $this->sessionTransparentMock, $this->secureTokenServiceMock, $this->sessionManagerMock, - $this->transparentMock + $this->transparentMock, + null, + $this->formKeyValidator ); } @@ -113,16 +133,15 @@ public function testExecuteSuccess() 'error' => false ]; - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->getMock(); $tokenMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $jsonMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) - ->disableOriginalConstructor() - ->getMock(); + $this->formKeyValidator->method('validate') + ->willReturn(true); $this->sessionManagerMock->expects($this->atLeastOnce()) ->method('getQuote') ->willReturn($quoteMock); @@ -147,15 +166,9 @@ public function testExecuteSuccess() ['securetoken', null, $secureToken] ] ); - $this->resultJsonFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($jsonMock); - $jsonMock->expects($this->once()) - ->method('setData') - ->with($resultExpectation) - ->willReturnSelf(); + $jsonResult = $this->getJsonResult($resultExpectation); - $this->assertEquals($jsonMock, $this->controller->execute()); + $this->assertEquals($jsonResult, $this->controller->execute()); } public function testExecuteTokenRequestException() @@ -167,13 +180,11 @@ public function testExecuteTokenRequestException() 'error_messages' => __('Your payment has been declined. Please try again.') ]; - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) - ->disableOriginalConstructor() - ->getMock(); - $jsonMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->getMock(); - + $this->formKeyValidator->method('validate') + ->willReturn(true); $this->sessionManagerMock->expects($this->atLeastOnce()) ->method('getQuote') ->willReturn($quoteMock); @@ -187,18 +198,21 @@ public function testExecuteTokenRequestException() ->method('requestToken') ->with($quoteMock) ->willThrowException(new \Exception()); - $this->resultJsonFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($jsonMock); - $jsonMock->expects($this->once()) - ->method('setData') - ->with($resultExpectation) - ->willReturnSelf(); - $this->assertEquals($jsonMock, $this->controller->execute()); + $jsonResult = $this->getJsonResult($resultExpectation); + + $this->assertEquals($jsonResult, $this->controller->execute()); } - public function testExecuteEmptyQuoteError() + /** + * Tests error generation. + * + * @param Quote|null $quote + * @param bool $isValidToken + * @return void + * @dataProvider executeErrorDataProvider + */ + public function testExecuteError($quote, bool $isValidToken) { $resultExpectation = [ 'success' => false, @@ -206,22 +220,51 @@ public function testExecuteEmptyQuoteError() 'error_messages' => __('Your payment has been declined. Please try again.') ]; - $quoteMock = null; - $jsonMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) + $this->sessionManagerMock->expects($this->atLeastOnce()) + ->method('getQuote') + ->willReturn($quote); + $this->formKeyValidator->method('validate') + ->willReturn($isValidToken); + + $jsonResult = $this->getJsonResult($resultExpectation); + + $this->assertEquals($jsonResult, $this->controller->execute()); + } + + /** + * @return array + */ + public function executeErrorDataProvider() + { + $quote = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->getMock(); - $this->sessionManagerMock->expects($this->atLeastOnce()) - ->method('getQuote') - ->willReturn($quoteMock); - $this->resultJsonFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($jsonMock); + return [ + 'empty quote' => [null, true], + 'invalid CSRF token' => [$quote, false] + ]; + } + + /** + * Returns json result. + * + * @param array $result + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getJsonResult(array $result): \PHPUnit_Framework_MockObject_MockObject + { + $jsonMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); $jsonMock->expects($this->once()) ->method('setData') - ->with($resultExpectation) + ->with($result) ->willReturnSelf(); + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($jsonMock); - $this->assertEquals($jsonMock, $this->controller->execute()); + return $jsonMock; } } diff --git a/app/code/Magento/Paypal/composer.json b/app/code/Magento/Paypal/composer.json index 331198b474783..5ee1739253ced 100644 --- a/app/code/Magento/Paypal/composer.json +++ b/app/code/Magento/Paypal/composer.json @@ -26,7 +26,7 @@ "magento/module-checkout-agreements": "100.2.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "proprietary" ], diff --git a/app/code/Magento/PaypalCaptcha/LICENSE.txt b/app/code/Magento/PaypalCaptcha/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt b/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php b/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php new file mode 100644 index 0000000000000..289a1631ed1f6 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalCaptcha\Model\Checkout; + +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\CaptchaInterface; +use Magento\Checkout\Model\ConfigProviderInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Configuration provider for Captcha rendering. + */ +class ConfigProviderPayPal implements ConfigProviderInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Data + */ + private $captchaData; + + /** + * @var string + */ + private static $formId = 'co-payment-form'; + + /** + * @param StoreManagerInterface $storeManager + * @param Data $captchaData + */ + public function __construct( + StoreManagerInterface $storeManager, + Data $captchaData + ) { + $this->storeManager = $storeManager; + $this->captchaData = $captchaData; + } + + /** + * @inheritdoc + */ + public function getConfig(): array + { + $config['captchaPayments'][self::$formId] = [ + 'isCaseSensitive' => $this->isCaseSensitive(self::$formId), + 'imageHeight' => $this->getImageHeight(self::$formId), + 'imageSrc' => $this->getImageSrc(self::$formId), + 'refreshUrl' => $this->getRefreshUrl(), + 'isRequired' => $this->isRequired(self::$formId), + 'timestamp' => time() + ]; + + return $config; + } + + /** + * Returns is captcha case sensitive + * + * @param string $formId + * @return bool + */ + private function isCaseSensitive(string $formId): bool + { + return (bool)$this->getCaptchaModel($formId)->isCaseSensitive(); + } + + /** + * Returns captcha image height + * + * @param string $formId + * @return int + */ + private function getImageHeight(string $formId): int + { + return (int)$this->getCaptchaModel($formId)->getHeight(); + } + + /** + * Returns captcha image source path + * + * @param string $formId + * @return string + */ + private function getImageSrc(string $formId): string + { + if ($this->isRequired($formId)) { + $captcha = $this->getCaptchaModel($formId); + $captcha->generate(); + return $captcha->getImgSrc(); + } + + return ''; + } + + /** + * Returns URL to controller action which returns new captcha image + * + * @return string + */ + private function getRefreshUrl(): string + { + $store = $this->storeManager->getStore(); + return $store->getUrl('captcha/refresh', ['_secure' => $store->isCurrentlySecure()]); + } + + /** + * Whether captcha is required to be inserted to this form + * + * @param string $formId + * @return bool + */ + private function isRequired(string $formId): bool + { + return (bool)$this->getCaptchaModel($formId)->isRequired(); + } + + /** + * Return captcha model for specified form + * + * @param string $formId + * @return CaptchaInterface + */ + private function getCaptchaModel(string $formId): CaptchaInterface + { + return $this->captchaData->getCaptcha($formId); + } +} diff --git a/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php b/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php new file mode 100644 index 0000000000000..e7cb282b1799b --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalCaptcha\Observer; + +use Magento\Captcha\Helper\Data; +use Magento\Framework\App\Action\Action; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\App\ActionFlag ; + +/** + * Validates Captcha for Request Token controller + */ +class CaptchaRequestToken implements ObserverInterface +{ + /** + * @var Data + */ + private $helper; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var ActionFlag + */ + private $actionFlag; + + /** + * @param Data $helper + * @param Json $jsonSerializer + * @param ActionFlag $actionFlag + */ + public function __construct(Data $helper, Json $jsonSerializer, ActionFlag $actionFlag) + { + $this->helper = $helper; + $this->jsonSerializer = $jsonSerializer; + $this->actionFlag = $actionFlag; + } + + /** + * @inheritdoc + */ + public function execute(Observer $observer) + { + $formId = 'co-payment-form'; + $captcha = $this->helper->getCaptcha($formId); + + if (!$captcha->isRequired()) { + return; + } + + /** @var Action $controller */ + $controller = $observer->getControllerAction(); + $word = $controller->getRequest()->getPost('captcha_string'); + if ($captcha->isCorrect($word)) { + return; + } + + $data = $this->jsonSerializer->serialize([ + 'success' => false, + 'error' => true, + 'error_messages' => __('Incorrect CAPTCHA.') + ]); + $this->actionFlag->set('', Action::FLAG_NO_DISPATCH, true); + $controller->getResponse()->representJson($data); + } +} diff --git a/app/code/Magento/PaypalCaptcha/README.md b/app/code/Magento/PaypalCaptcha/README.md new file mode 100644 index 0000000000000..71588599a5ecd --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/README.md @@ -0,0 +1 @@ +The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form. \ No newline at end of file diff --git a/app/code/Magento/PaypalCaptcha/composer.json b/app/code/Magento/PaypalCaptcha/composer.json new file mode 100644 index 0000000000000..c2c7032ae8060 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-paypal-captcha", + "description": "N/A", + "require": { + "php": "~7.0.13|~7.1.0", + "magento/framework": "101.0.*", + "magento/module-captcha": "100.2.*", + "magento/module-checkout": "100.2.*", + "magento/module-store": "100.2.*" + }, + "suggest": { + "magento/module-paypal": "100.2.*" + }, + "type": "magento2-module", + "version": "100.2.0", + "license": [ + "proprietary" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\PaypalCaptcha\\": "" + } + } +} diff --git a/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml b/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..12afd8ceda60e --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="customer"> + <group id="captcha" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="forms" translate="label comment" type="multiselect" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <comment>CAPTCHA for "Create user", "Forgot password", "Payflow Pro" forms is always enabled if chosen.</comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/config.xml b/app/code/Magento/PaypalCaptcha/etc/config.xml new file mode 100644 index 0000000000000..133a78a42f7b4 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/config.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <customer> + <captcha> + <shown_to_logged_in_user> + <co-payment-form>1</co-payment-form> + </shown_to_logged_in_user> + <always_for> + <co-payment-form>1</co-payment-form> + </always_for> + </captcha> + </customer> + <captcha translate="label"> + <frontend> + <areas> + <co-payment-form> + <label>Payflow Pro</label> + </co-payment-form> + </areas> + </frontend> + </captcha> + </default> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml new file mode 100644 index 0000000000000..c236d5ea04ca0 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Checkout\Model\CompositeConfigProvider"> + <arguments> + <argument name="configProviders" xsi:type="array"> + <item name="paypal_captcha_config_provider" xsi:type="object">Magento\PaypalCaptcha\Model\Checkout\ConfigProviderPayPal</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml new file mode 100644 index 0000000000000..ae706c4485d61 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="controller_action_predispatch_paypal_transparent_requestsecuretoken"> + <observer name="captcha_request_token" instance="Magento\PaypalCaptcha\Observer\CaptchaRequestToken"/> + </event> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/module.xml b/app/code/Magento/PaypalCaptcha/etc/module.xml new file mode 100644 index 0000000000000..a456cb0584fe6 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/module.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_PaypalCaptcha" setup_version="2.0.0"> + <sequence> + <module name="Magento_Captcha"/> + <module name="Magento_Paypal"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/PaypalCaptcha/registration.php b/app/code/Magento/PaypalCaptcha/registration.php new file mode 100644 index 0000000000000..4dac0582a6d1b --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_PaypalCaptcha', __DIR__); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/PaypalCaptcha/view/frontend/layout/checkout_index_index.xml new file mode 100644 index 0000000000000..9837068faab73 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/layout/checkout_index_index.xml @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="checkout.root"> + <arguments> + <argument name="jsLayout" xsi:type="array"> + <item name="components" xsi:type="array"> + <item name="checkout" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="steps" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="billing-step" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="payment" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="payments-list" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="paypal-captcha" xsi:type="array"> + <item name="component" xsi:type="string">uiComponent</item> + <item name="displayArea" xsi:type="string">paypal-captcha</item> + <item name="dataScope" xsi:type="string">paypal-captcha</item> + <item name="provider" xsi:type="string">checkoutProvider</item> + <item name="config" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Checkout/payment/before-place-order</item> + </item> + <item name="children" xsi:type="array"> + <item name="captcha" xsi:type="array"> + <item name="component" xsi:type="string">Magento_PaypalCaptcha/js/view/checkout/paymentCaptcha</item> + <item name="displayArea" xsi:type="string">paypal-captcha</item> + <item name="formId" xsi:type="string">co-payment-form</item> + <item name="configSource" xsi:type="string">checkoutConfig</item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..78e7add4ec690 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js @@ -0,0 +1,14 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + config: { + mixins: { + 'Magento_Checkout/js/view/payment/list': { + 'Magento_PaypalCaptcha/js/view/payment/list-mixin': true + } + } + } +}; diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js new file mode 100644 index 0000000000000..f8f119e3b3396 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js @@ -0,0 +1,44 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'Magento_Captcha/js/model/captcha' +], +function ($, defaultCaptcha, captchaList, Captcha) { + 'use strict'; + + return defaultCaptcha.extend({ + + /** @inheritdoc */ + initialize: function () { + var captchaConfigPayment, + currentCaptcha; + + this._super(); + + if (window[this.configSource] && window[this.configSource].captchaPayments) { + captchaConfigPayment = window[this.configSource].captchaPayments; + + $.each(captchaConfigPayment, function (formId, captchaData) { + var captcha; + + captchaData.formId = formId; + captcha = Captcha(captchaData); + captchaList.add(captcha); + }); + } + + currentCaptcha = captchaList.getCaptchaByFormId(this.formId); + + if (currentCaptcha != null) { + currentCaptcha.setIsVisible(true); + this.setCurrentCaptcha(currentCaptcha); + } + } + }); +}); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js new file mode 100644 index 0000000000000..60172f696e9ed --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js @@ -0,0 +1,54 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Captcha/js/model/captchaList' +], function ($, captchaList) { + 'use strict'; + + var mixin = { + + formId: 'co-payment-form', + + /** + * Sets custom template for Payflow Pro + * + * @param {Object} payment + * @returns {Object} + */ + createComponent: function (payment) { + + var component = this._super(payment); + + if (component.component === 'Magento_Paypal/js/view/payment/method-renderer/payflowpro-method') { + component.template = 'Magento_PaypalCaptcha/payment/payflowpro-form'; + $(window).off('clearTimeout') + .on('clearTimeout', this.clearTimeout.bind(this)); + } + + return component; + }, + + /** + * Overrides default window.clearTimeout() to catch errors from iframe and reload Captcha. + */ + clearTimeout: function () { + var captcha = captchaList.getCaptchaByFormId(this.formId); + + if (captcha !== null) { + captcha.refresh(); + } + clearTimeout(); + } + }; + + /** + * Overrides `Magento_Checkout/js/view/payment/list::createComponent` + */ + return function (target) { + return target.extend(mixin); + }; +}); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html new file mode 100644 index 0000000000000..fec5cf96b0324 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html @@ -0,0 +1,90 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}"> + <div class="payment-method-title field choice"> + <input type="radio" + name="payment[method]" + class="radio" + data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/> + <label class="label" data-bind="attr: {'for': getCode()}"> + <span data-bind="text: getTitle()"></span> + </label> + </div> + + <div class="payment-method-content"> + <!-- ko foreach: getRegion('messages') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> + <div class="payment-method-billing-address"> + <!-- ko foreach: $parent.getRegion(getBillingAddressFormName()) --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> + </div> + <iframe width="0" + height="0" + data-bind="src: getSource(), attr: {id: getCode() + '-transparent-iframe', 'data-container': getCode() + '-transparent-iframe'}" + allowtransparency="true" + frameborder="0" + name="iframeTransparent" + class="payment-method-iframe"> + </iframe> + <form class="form" id="co-transparent-form" action="#" method="post" data-bind="mageInit: { + 'transparent':{ + 'context': context(), + 'controller': getControllerName(), + 'gateway': getCode(), + 'orderSaveUrl':getPlaceOrderUrl(), + 'cgiUrl': getCgiUrl(), + 'dateDelim': getDateDelim(), + 'cardFieldsMap': getCardFieldsMap(), + 'nativeAction': getSaveOrderUrl() + }, 'validation':[]}"> + + <!-- ko template: 'Magento_Payment/payment/cc-form' --><!-- /ko --> + + <!-- ko if: (isVaultEnabled())--> + <div class="field-tooltip-content"> + <input type="checkbox" + name="vault[is_enabled]" + class="checkbox-inline" + data-bind="attr: {'id': getCode() + '_enable_vault'}, checked: vaultEnabler.isActivePaymentTokenEnabler"/> + <label class="label" data-bind="attr: {'for': getCode() + '_enable_vault'}"> + <span><!-- ko i18n: 'Save credit card information for future use.'--><!-- /ko --></span> + </label> + </div> + <!-- /ko --> + </form> + <fieldset class="fieldset payment items ccard"> + <!-- ko foreach: $parent.getRegion('paypal-captcha') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!-- /ko --> + </fieldset> + + + <div class="checkout-agreements-block"> + <!-- ko foreach: $parent.getRegion('before-place-order') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> + </div> + <div class="actions-toolbar"> + <div class="primary"> + <button data-role="review-save" + type="submit" + data-bind=" + attr: {title: $t('Place Order')}, + enable: (getCode() == isChecked()), + click: placeOrder, + css: {disabled: !isPlaceOrderActionAllowed()} + " + class="action primary checkout" + disabled> + <span data-bind="i18n: 'Place Order'"></span> + </button> + </div> + </div> + </div> +</div> diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php index 35c2c70be30dc..8ae22e4c26c6f 100644 --- a/app/code/Magento/Persistent/Model/QuoteManager.php +++ b/app/code/Magento/Persistent/Model/QuoteManager.php @@ -7,6 +7,8 @@ /** * Class QuoteManager + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class QuoteManager { @@ -87,6 +89,7 @@ public function setGuest($checkQuote = false) ->setCustomerLastname(null) ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID) ->setIsPersistent(false) + ->setCustomerIsGuest(true) ->removeAllAddresses(); //Create guest addresses $quote->getShippingAddress(); diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php index 42baf7d692a7c..7e5a5769e00a5 100644 --- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php +++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -11,6 +10,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Observer of expired session + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class CheckExpirePersistentQuoteObserver implements ObserverInterface { /** @@ -110,8 +114,12 @@ public function execute(\Magento\Framework\Event\Observer $observer) !$this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn() && $this->_checkoutSession->getQuoteId() && - !$this->isRequestFromCheckoutPage($this->request) + !$this->isRequestFromCheckoutPage($this->request) && // persistent session does not expire on onepage checkout page + ( + $this->_checkoutSession->getQuote()->getIsPersistent() || + $this->_checkoutSession->getQuote()->getCustomerIsGuest() + ) ) { $this->_eventManager->dispatch('persistent_session_expired'); $this->quoteManager->expire(); diff --git a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php index db6b6d1ee370d..2803bc998dcbe 100644 --- a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php +++ b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Observer for setting "is_persistent" value to quote + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class SetQuotePersistentDataObserver implements ObserverInterface { /** @@ -73,8 +77,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) } if (( - ($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) - && !$this->_persistentData->isShoppingCartPersist() + ($this->_persistentSession->isPersistent()) + && $this->_persistentData->isShoppingCartPersist() ) && $this->quoteManager->isPersistent() ) { diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontQuoteShippingDataPersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontQuoteShippingDataPersistedForGuestTest.xml new file mode 100644 index 0000000000000..908a037ed36a2 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontQuoteShippingDataPersistedForGuestTest.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontQuoteShippingDataPersistedForGuestTest"> + <annotations> + <features value="Persistent"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Estimate Shipping and Tax block sections on shipping cart saving correctly for Guest."/> + <description value="Verify that 'Estimate Shipping and Tax' block sections on shipping cart saving correctly for Guest after switching to another page. And check that the shopping cart is cleared after reset persistent cookie."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-99048"/> + <useCaseId value="MAGETWO-98569"/> + <group value="persistent"/> + <group value="checkout"/> + </annotations> + <before> + <!--Enabled The Persistent Shopping Cart feature --> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisabled" stepKey="persistentLogoutClearDisable"/> + <!--Create category and simple product--> + <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> + <createData entity="SimpleProduct2" stepKey="createProduct"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + </before> + <after> + <!--Revert persistent configuration to default--> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSubCategory" stepKey="deleteSubCategory"/> + </after> + <!--Step 1: Login as a Customer with remember me checked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeChecked" stepKey="loginToStorefrontAccountWithRememberMeChecked"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Step 2: Open the Product Page and add the product to shopping cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPageAsLoggedUser"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCartAsLoggedUser"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <!--Step 3: Log out, reset persistent cookie and go to homepage--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> + <resetCookie userInput="persistent_shopping_cart" stepKey="resetPersistentCookie"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePageAfterResetPersistentCookie"/> + <!--Check that the minicart is empty--> + <actionGroup ref="AssertMiniCartEmpty" after="amOnHomePageAfterResetPersistentCookie" stepKey="seeMinicartEmpty"/> + <!--Step 4: Add the product to shopping cart and open cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPageAsGuestUser"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCartAsGuestUser"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartBeforeChangeShippingAndTaxSection"/> + <!--Step 5: Open Estimate Shipping and Tax block and fill the sections--> + <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTax" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <selectOption selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="selectUSCountry"/> + <waitForPageLoad stepKey="waitAfterSelectCountry"/> + <selectOption selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{US_Address_CA.state}}" stepKey="selectCaliforniaRegion"/> + <waitForPageLoad stepKey="waitAfterSelectRegion"/> + <fillField selector="{{StorefrontCheckoutCartSummarySection.postcode}}" userInput="{{US_Address_CA.postcode}}" stepKey="inputPostCode"/> + <waitForPageLoad stepKey="waitAfterSelectPostcode"/> + <!--Step 6: Go to Homepage--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePageAfterChangingShippingAndTaxSection"/> + <!--Step 7: Go to shopping cart and check "Estimate Shipping and Tax" fields values are saved--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" after="goToHomePageAfterChangingShippingAndTaxSection" stepKey="goToShoppingCartAfterChangingShippingAndTaxSection"/> + <conditionalClick selector="{{StorefrontCheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{StorefrontCheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTaxAfterChanging" /> + <waitForLoadingMaskToDisappear stepKey="waitEstimateBlock"/> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{StorefrontCheckoutCartSummarySection.region}}" userInput="{{US_Address_CA.state}}" stepKey="checkCustomerRegion" /> + <grabValueFrom selector="{{StorefrontCheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php index 29a3196c5f45e..b6e88d4363b90 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Persistent\Test\Unit\Observer; +use Magento\Quote\Model\Quote; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CheckExpirePersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -54,6 +58,11 @@ class CheckExpirePersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase */ private $requestMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Quote + */ + private $quoteMock; + protected function setUp() { $this->sessionMock = $this->createMock(\Magento\Persistent\Helper\Session::class); @@ -79,6 +88,10 @@ protected function setUp() $this->checkoutSessionMock, $this->requestMock ); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->setMethods(['getCustomerIsGuest', 'getIsPersistent']) + ->disableOriginalConstructor() + ->getMock(); } public function testExecuteWhenCanNotApplyPersistentData() @@ -128,6 +141,11 @@ public function testExecuteWhenPersistentIsEnabled( ->will($this->returnValue(true)); $this->persistentHelperMock->expects($this->once())->method('isEnabled')->will($this->returnValue(true)); $this->sessionMock->expects($this->once())->method('isPersistent')->will($this->returnValue(false)); + $this->checkoutSessionMock + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteMock->method('getCustomerIsGuest')->willReturn(true); + $this->quoteMock->method('getIsPersistent')->willReturn(true); $this->customerSessionMock ->expects($this->atLeastOnce()) ->method('isLoggedIn') diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php index 6724743789cea..ffa829e8456cc 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php @@ -7,6 +7,9 @@ namespace Magento\Persistent\Test\Unit\Observer; +/** + * Observer test for setting "is_persistent" value to quote + */ class SetQuotePersistentDataObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -83,7 +86,6 @@ public function testExecuteWhenQuoteNotExist() ->method('getEvent') ->will($this->returnValue($this->eventManagerMock)); $this->eventManagerMock->expects($this->once())->method('getQuote'); - $this->customerSessionMock->expects($this->never())->method('isLoggedIn'); $this->model->execute($this->observerMock); } @@ -98,8 +100,7 @@ public function testExecuteWhenSessionIsPersistent() ->expects($this->once()) ->method('getQuote') ->will($this->returnValue($this->quoteMock)); - $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->will($this->returnValue(false)); - $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(false)); + $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(true)); $this->quoteManagerMock->expects($this->once())->method('isPersistent')->will($this->returnValue(true)); $this->quoteMock->expects($this->once())->method('setIsPersistent')->with(true); $this->model->execute($this->observerMock); diff --git a/app/code/Magento/Persistent/composer.json b/app/code/Magento/Persistent/composer.json index 73184a0648d24..0bfbc30e28f69 100644 --- a/app/code/Magento/Persistent/composer.json +++ b/app/code/Magento/Persistent/composer.json @@ -12,7 +12,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php b/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php index 9950526182e3e..53705ee9f6e72 100644 --- a/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php +++ b/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php @@ -107,6 +107,9 @@ public function execute() { $baseTmpMediaPath = $this->mediaConfig->getBaseTmpMediaPath(); try { + if (!$this->getRequest()->isPost()) { + throw new LocalizedException(__('Invalid request type.')); + } $remoteFileUrl = $this->getRequest()->getParam('remote_image'); $this->validateRemoteFile($remoteFileUrl); $localFileName = Uploader::getCorrectFileName(basename($remoteFileUrl)); diff --git a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php index 28c6db7d31379..cc38e55d26af2 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php @@ -6,6 +6,7 @@ namespace Magento\ProductVideo\Test\Unit\Controller\Adminhtml\Product\Gallery; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Request\Http; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -101,14 +102,16 @@ protected function setUp() $this->adapterFactoryMock->expects($this->once())->method('create')->willReturn($this->abstractAdapter); $this->curlMock = $this->createMock(\Magento\Framework\HTTP\Adapter\Curl::class); $this->storageFileMock = $this->createMock(\Magento\MediaStorage\Model\ResourceModel\File\Storage\File::class); - $this->request = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['isPost']) + ->getMockForAbstractClass(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->fileDriverMock = $this->createMock(\Magento\Framework\Filesystem\DriverInterface::class); - $this->contextMock->expects($this->any())->method('getRequest')->will($this->returnValue($this->request)); $managerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) ->disableOriginalConstructor() ->setMethods(['get']) ->getMockForAbstractClass(); - $this->contextMock->expects($this->any())->method('getRequest')->will($this->returnValue($this->request)); + $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->request); $this->contextMock->expects($this->any())->method('getObjectManager')->willReturn($managerMock); $this->image = $objectManager->getObject( diff --git a/app/code/Magento/ProductVideo/composer.json b/app/code/Magento/ProductVideo/composer.json index 7c5017eef4a5a..d92ab26d39fe5 100644 --- a/app/code/Magento/ProductVideo/composer.json +++ b/app/code/Magento/ProductVideo/composer.json @@ -16,7 +16,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "proprietary" ], diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index 6ea70e63fdbf6..064b0c60e199c 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -7,6 +7,7 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\GroupInterface; +use Magento\Directory\Model\AllowedCountries; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\Model\AbstractExtensibleModel; @@ -14,6 +15,7 @@ use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Address\Total as AddressTotal; use Magento\Sales\Model\Status; +use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\ObjectManager; use Magento\Sales\Model\OrderIncrementIdChecker; @@ -360,6 +362,11 @@ class Quote extends AbstractExtensibleModel implements \Magento\Quote\Api\Data\C */ private $orderIncrementIdChecker; + /** + * @var AllowedCountries + */ + private $allowedCountriesReader; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -402,6 +409,7 @@ class Quote extends AbstractExtensibleModel implements \Magento\Quote\Api\Data\C * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param OrderIncrementIdChecker|null $orderIncrementIdChecker + * @param AllowedCountries|null $allowedCountriesReader * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -445,7 +453,8 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - OrderIncrementIdChecker $orderIncrementIdChecker = null + OrderIncrementIdChecker $orderIncrementIdChecker = null, + AllowedCountries $allowedCountriesReader = null ) { $this->quoteValidator = $quoteValidator; $this->_catalogProduct = $catalogProduct; @@ -482,6 +491,8 @@ public function __construct( $this->shippingAssignmentFactory = $shippingAssignmentFactory; $this->orderIncrementIdChecker = $orderIncrementIdChecker ?: ObjectManager::getInstance() ->get(OrderIncrementIdChecker::class); + $this->allowedCountriesReader = $allowedCountriesReader + ?: ObjectManager::getInstance()->get(AllowedCountries::class); parent::__construct( $context, $registry, @@ -941,7 +952,7 @@ public function assignCustomerWithAddressChange( /** @var \Magento\Quote\Model\Quote\Address $billingAddress */ $billingAddress = $this->_quoteAddressFactory->create(); $billingAddress->importCustomerAddressData($defaultBillingAddress); - $this->setBillingAddress($billingAddress); + $this->assignAddress($billingAddress); } } @@ -959,7 +970,7 @@ public function assignCustomerWithAddressChange( $shippingAddress = $this->_quoteAddressFactory->create(); } } - $this->setShippingAddress($shippingAddress); + $this->assignAddress($shippingAddress, false); } return $this; @@ -2570,4 +2581,34 @@ public function setExtensionAttributes(\Magento\Quote\Api\Data\CartExtensionInte { return $this->_setExtensionAttributes($extensionAttributes); } + + /** + * Check is address allowed for store + * + * @param Address $address + * @param int|null $storeId + * @return bool + */ + private function isAddressAllowedForWebsite(Address $address, $storeId): bool + { + $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_STORE, $storeId); + + return in_array($address->getCountryId(), $allowedCountries); + } + + /** + * Assign address to quote + * + * @param Address $address + * @param bool $isBillingAddress + * @return void + */ + private function assignAddress(Address $address, bool $isBillingAddress = true) + { + if ($this->isAddressAllowedForWebsite($address, $this->getStoreId())) { + $isBillingAddress + ? $this->setBillingAddress($address) + : $this->setShippingAddress($address); + } + } } diff --git a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php index c5b8dc1c4b124..81b0bed2592ea 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php +++ b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php @@ -12,6 +12,9 @@ use Magento\Quote\Model\QuoteAddressValidator; use Magento\Customer\Api\AddressRepositoryInterface; +/** + * Saves billing address for quotes. + */ class BillingAddressPersister { /** @@ -37,17 +40,17 @@ public function __construct( } /** + * Save address for billing. + * * @param CartInterface $quote * @param AddressInterface $address * @param bool $useForShipping * @return void - * @throws NoSuchEntityException - * @throws InputException */ public function save(CartInterface $quote, AddressInterface $address, $useForShipping = false) { /** @var \Magento\Quote\Model\Quote $quote */ - $this->addressValidator->validate($address); + $this->addressValidator->validateForCart($quote, $address); $customerAddressId = $address->getCustomerAddressId(); $shippingAddress = null; $addressData = []; diff --git a/app/code/Magento/Quote/Model/Quote/Item/Compare.php b/app/code/Magento/Quote/Model/Quote/Item/Compare.php index ddaa636ef32b3..76ba324518dc1 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Compare.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Compare.php @@ -50,7 +50,7 @@ protected function getOptionValues($value) if (is_string($value) && $this->jsonValidator->isValid($value)) { $value = $this->serializer->unserialize($value); if (is_array($value)) { - unset($value['qty'], $value['uenc']); + unset($value['qty'], $value['uenc'], $value['related_product'], $value['item']); $value = array_filter($value, function ($optionValue) { return !empty($optionValue); }); diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php index 9a86829bfc4ce..06f21ee119bd5 100644 --- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php +++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php @@ -6,10 +6,13 @@ namespace Magento\Quote\Model; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; /** * Quote shipping/billing address validator service. * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class QuoteAddressValidator { @@ -28,7 +31,7 @@ class QuoteAddressValidator protected $customerRepository; /** - * @var \Magento\Customer\Model\Session + * @deprecated This class is not a part of HTML presentation layer and should not use sessions. */ protected $customerSession; @@ -50,44 +53,68 @@ public function __construct( } /** - * Validates the fields in a specified address data object. + * Validate address. * - * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. - * @return bool - * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. - * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + * @param AddressInterface $address + * @param int|null $customerId Cart belongs to + * @return void + * @throws NoSuchEntityException The specified customer ID or address ID is not valid. */ - public function validate(\Magento\Quote\Api\Data\AddressInterface $addressData) + private function doValidate(AddressInterface $address, $customerId) { //validate customer id - if ($addressData->getCustomerId()) { - $customer = $this->customerRepository->getById($addressData->getCustomerId()); - if (!$customer->getId()) { - throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer id %1', $addressData->getCustomerId()) - ); + if ($customerId) { + try { + $this->customerRepository->getById($customerId); + } catch (NoSuchEntityException $exception) { + throw new NoSuchEntityException(__('Invalid customer id %1', $customerId)); } } - if ($addressData->getCustomerAddressId()) { + if ($address->getCustomerAddressId()) { + //Existing address cannot belong to a guest + if (!$customerId) { + throw new NoSuchEntityException(__('Invalid customer address id %1', $address->getCustomerAddressId())); + } + //Validating address ID try { - $this->addressRepository->getById($addressData->getCustomerAddressId()); + $this->addressRepository->getById($address->getCustomerAddressId()); } catch (NoSuchEntityException $e) { - throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid address id %1', $addressData->getId()) - ); + throw new NoSuchEntityException(__('Invalid address id %1', $address->getId())); } - + //Finding available customer's addresses $applicableAddressIds = array_map(function ($address) { /** @var \Magento\Customer\Api\Data\AddressInterface $address */ return $address->getId(); - }, $this->customerRepository->getById($addressData->getCustomerId())->getAddresses()); - if (!in_array($addressData->getCustomerAddressId(), $applicableAddressIds)) { - throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer address id %1', $addressData->getCustomerAddressId()) - ); + }, $this->customerRepository->getById($customerId)->getAddresses()); + if (!in_array($address->getCustomerAddressId(), $applicableAddressIds)) { + throw new NoSuchEntityException(__('Invalid customer address id %1', $address->getCustomerAddressId())); } } + } + + /** + * Validates the fields in a specified address data object. + * + * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. + * @return bool + */ + public function validate(AddressInterface $addressData): bool + { + $this->doValidate($addressData, $addressData->getCustomerId()); + return true; } + + /** + * Validate address to be used for cart. + * + * @param CartInterface $cart + * @param AddressInterface $address + * @return void + */ + public function validateForCart(CartInterface $cart, AddressInterface $address) + { + $this->doValidate($address, $cart->getCustomerIsGuest() ? null : $cart->getCustomer()->getId()); + } } diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index e21ae7fa1af37..b7d994138026a 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -529,19 +529,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ); $this->quoteRepository->save($quote); } catch (\Exception $e) { - if (!empty($this->addressesToSync)) { - foreach ($this->addressesToSync as $addressId) { - $this->addressRepository->deleteById($addressId); - } - } - $this->eventManager->dispatch( - 'sales_model_service_quote_submit_failure', - [ - 'order' => $order, - 'quote' => $quote, - 'exception' => $e - ] - ); + $this->rollbackAddresses($quote, $order, $e); throw $e; } return $order; @@ -608,4 +596,42 @@ protected function _prepareCustomerQuote($quote) $shipping->setIsDefaultBilling(true); } } + + /** + * Remove related to order and quote addresses and submit exception to further processing. + * + * @param Quote $quote + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Exception $e + * @throws \Exception + * @return void + */ + private function rollbackAddresses( + QuoteEntity $quote, + \Magento\Sales\Api\Data\OrderInterface $order, + \Exception $e + ) { + try { + if (!empty($this->addressesToSync)) { + foreach ($this->addressesToSync as $addressId) { + $this->addressRepository->deleteById($addressId); + } + } + $this->eventManager->dispatch( + 'sales_model_service_quote_submit_failure', + [ + 'order' => $order, + 'quote' => $quote, + 'exception' => $e, + ] + ); + } catch (\Exception $consecutiveException) { + $message = sprintf( + "An exception occurred on 'sales_model_service_quote_submit_failure' event: %s", + $consecutiveException->getMessage() + ); + + throw new \Exception($message, 0, $e); + } + } } diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php index 309c89e3702f5..959604592c848 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php @@ -325,10 +325,10 @@ private function getOptionProductIds( /** * Check is valid product. * - * @param ProductInterface $product + * @param ProductInterface|null $product * @return bool */ - private function isValidProduct(ProductInterface $product): bool + private function isValidProduct(ProductInterface $product = null): bool { $result = ($product && (int)$product->getStatus() !== ProductStatus::STATUS_DISABLED); diff --git a/app/code/Magento/Quote/Model/ShippingAddressManagement.php b/app/code/Magento/Quote/Model/ShippingAddressManagement.php index 0e2be5c9e3692..71a93e4604200 100644 --- a/app/code/Magento/Quote/Model/ShippingAddressManagement.php +++ b/app/code/Magento/Quote/Model/ShippingAddressManagement.php @@ -78,7 +78,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc * @SuppressWarnings(PHPMD.NPathComplexity) */ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address) @@ -94,7 +94,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres $saveInAddressBook = $address->getSaveInAddressBook() ? 1 : 0; $sameAsBilling = $address->getSameAsBilling() ? 1 : 0; $customerAddressId = $address->getCustomerAddressId(); - $this->addressValidator->validate($address); + $this->addressValidator->validateForCart($quote, $address); $quote->setShippingAddress($address); $address = $quote->getShippingAddress(); @@ -122,7 +122,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres } /** - * {@inheritDoc} + * @inheritdoc */ public function get($cartId) { diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php index 08f5f6a808561..c0ffbc997590e 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php @@ -67,27 +67,22 @@ public function testValidateInvalidCustomer() { $customerId = 100; $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); $address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($customerId); $this->customerRepositoryMock->expects($this->once())->method('getById')->with($customerId) - ->willReturn($customerMock); + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); $this->model->validate($address); } /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage Invalid address id 101 + * @expectedExceptionMessage Invalid customer address id 101 */ public function testValidateInvalidAddress() { $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); $this->customerRepositoryMock->expects($this->never())->method('getById'); - $address->expects($this->atLeastOnce())->method('getCustomerAddressId')->willReturn(101); - $address->expects($this->once())->method('getId')->willReturn(101); - - $this->addressRepositoryMock->expects($this->once())->method('getById') - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + $address->expects($this->exactly(2))->method('getCustomerAddressId')->willReturn(101); $this->model->validate($address); } @@ -115,7 +110,6 @@ public function testValidateWithValidAddress() $customerAddress = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); $this->customerRepositoryMock->expects($this->exactly(2))->method('getById')->willReturn($customerMock); - $customerMock->expects($this->once())->method('getId')->willReturn($addressCustomer); $this->addressRepositoryMock->expects($this->once())->method('getById')->willReturn($this->quoteAddressMock); $this->quoteAddressMock->expects($this->any())->method('getCustomerId')->willReturn($addressCustomer); diff --git a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php index 59445c3999899..cc7cc49e11c81 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php @@ -110,7 +110,7 @@ public function testSetAddressValidationFailed() ->with('cart654') ->will($this->returnValue($quoteMock)); - $this->validatorMock->expects($this->once())->method('validate') + $this->validatorMock->expects($this->once())->method('validateForCart') ->will($this->throwException(new \Magento\Framework\Exception\NoSuchEntityException(__('error345')))); $this->service->assign('cart654', $this->quoteAddressMock); @@ -143,8 +143,8 @@ public function testSetAddress() ->with($customerAddressId) ->willReturn($customerAddressMock); - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) + $this->validatorMock->expects($this->once())->method('validateForCart') + ->with($quoteMock, $this->quoteAddressMock) ->willReturn(true); $quoteMock->expects($this->exactly(3))->method('getShippingAddress')->willReturn($this->quoteAddressMock); @@ -218,8 +218,8 @@ public function testSetAddressWithInabilityToSaveQuote() ->with($customerAddressId) ->willReturn($customerAddressMock); - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) + $this->validatorMock->expects($this->once())->method('validateForCart') + ->with($quoteMock, $this->quoteAddressMock) ->willReturn(true); $this->quoteAddressMock->expects($this->once())->method('getSaveInAddressBook')->willReturn(1); diff --git a/app/code/Magento/Quote/composer.json b/app/code/Magento/Quote/composer.json index 5391d7779b420..202a5b601ceab 100644 --- a/app/code/Magento/Quote/composer.json +++ b/app/code/Magento/Quote/composer.json @@ -23,7 +23,7 @@ "magento/module-webapi": "100.2.*" }, "type": "magento2-module", - "version": "101.0.6", + "version": "101.0.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php index 68f2722ca6dfb..b1f5628a0b7a3 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php @@ -147,15 +147,19 @@ protected function _showLastExecutionTime($flagCode, $refreshCode) } $refreshStatsLink = $this->getUrl('reports/report_statistics'); - $directRefreshLink = $this->getUrl('reports/report_statistics/refreshRecent', ['code' => $refreshCode]); + $directRefreshLink = $this->getUrl('reports/report_statistics/refreshRecent'); $this->messageManager->addNotice( __( 'Last updated: %1. To refresh last day\'s <a href="%2">statistics</a>, ' . - 'click <a href="%3">here</a>.', + 'click <a href="#2" data-post="%3">here</a>.', $updatedAt, $refreshStatsLink, - $directRefreshLink + str_replace( + '"', + '"', + json_encode(['action' => $directRefreshLink, 'data' => ['code' => $refreshCode]]) + ) ) ); return $this; diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php index 1f0f6e8e40535..957b1160d0281 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php @@ -6,15 +6,22 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Statistics; +use Magento\Framework\Exception\NotFoundException; + class RefreshRecent extends \Magento\Reports\Controller\Adminhtml\Report\Statistics { /** * Refresh statistics for last 25 hours * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + try { $collectionsNames = $this->_getCollectionNames(); /** @var \DateTime $currentDate */ diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index fd9adbe734101..d89a118bff94b 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -445,7 +445,7 @@ public function getDateRange($range, $customStart, $customEnd, $returnObjects = break; case 'custom': - $dateStart = $customStart ? $customStart : $dateEnd; + $dateStart = $customStart ? $customStart : $dateStart; $dateEnd = $customEnd ? $customEnd : $dateEnd; break; @@ -769,11 +769,12 @@ public function addOrdersCount() */ public function addRevenueToSelect($convertCurrency = false) { - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( !$convertCurrency, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns(['revenue' => $expr]); @@ -791,11 +792,12 @@ public function addSumAvgTotals($storeId = 0) /** * calculate average and total amount */ - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( $storeId, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns( @@ -808,13 +810,15 @@ public function addSumAvgTotals($storeId = 0) } /** - * Get SQL expression for totals + * Get SQL expression for totals. * * @param int $storeId * @param string $baseSubtotalRefunded * @param string $baseSubtotalCanceled * @param string $baseDiscountCanceled * @return string + * @deprecated + * @see getTotalsExpressionWithDiscountRefunded */ protected function getTotalsExpression( $storeId, @@ -825,10 +829,40 @@ protected function getTotalsExpression( $template = ($storeId != 0) ? '(main_table.base_subtotal - %2$s - %1$s - ABS(main_table.base_discount_amount) - %3$s)' : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) + %3$s) ' - . ' * main_table.base_to_global_rate)'; + . ' * main_table.base_to_global_rate)'; return sprintf($template, $baseSubtotalRefunded, $baseSubtotalCanceled, $baseDiscountCanceled); } + /** + * Get SQL expression for totals with discount refunded. + * + * @param int $storeId + * @param string $baseSubtotalRefunded + * @param string $baseSubtotalCanceled + * @param string $baseDiscountRefunded + * @param string $baseDiscountCanceled + * @return string + */ + private function getTotalsExpressionWithDiscountRefunded( + $storeId, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ) { + $template = ($storeId != 0) + ? '(main_table.base_subtotal - %2$s - %1$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s))' + : '((main_table.base_subtotal - %1$s - %2$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s)) ' + . ' * main_table.base_to_global_rate)'; + return sprintf( + $template, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ); + } + /** * Sort order by total amount * diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php index 1985db0b90e2a..2009cd3ff9d92 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php @@ -4,16 +4,16 @@ * See COPYING.txt for license details. */ +namespace Magento\Reports\Model\ResourceModel\Product\Downloads; + /** * Product Downloads Report collection * * @author Magento Core Team <core@magentocommerce.com> - */ -namespace Magento\Reports\Model\ResourceModel\Product\Downloads; - -/** + * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -97,4 +97,14 @@ public function addFieldToFilter($field, $condition = null) } return $this; } + + /** + * @inheritDoc + */ + public function getSelectCountSql() + { + $countSelect = parent::getSelectCountSql(); + $countSelect->reset(\Zend\Db\Sql\Select::GROUP); + return $countSelect; + } } diff --git a/app/code/Magento/Reports/composer.json b/app/code/Magento/Reports/composer.json index 1abd03339a22a..f9c0c3568b077 100644 --- a/app/code/Magento/Reports/composer.json +++ b/app/code/Magento/Reports/composer.json @@ -22,7 +22,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php index d6868eae6fcbc..d4079bbed3e3c 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Review\Block\Adminhtml; /** @@ -56,6 +57,7 @@ public function __construct( * * @return void * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _construct() { @@ -159,16 +161,16 @@ protected function _construct() } if ($this->getRequest()->getParam('ret', false) == 'pending') { - $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('catalog/*/pending') . '\')'); + $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('review/*/pending') . '\')'); $this->buttonList->update( 'delete', 'onclick', 'deleteConfirm(' . '\'' . __( 'Are you sure you want to do this?' - ) . '\' ' . '\'' . $this->getUrl( + ) . '\', ' . '\'' . $this->getUrl( '*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId), 'ret' => 'pending'] - ) . '\'' . ')' + ) . '\'' . ', {data: {}})' ); $this->_coreRegistry->register('ret', 'pending'); } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php index 75015d65e1a18..68f178911dc7c 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php @@ -18,20 +18,23 @@ public function execute() /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $reviewId = $this->getRequest()->getParam('id', false); - try { - $this->reviewFactory->create()->setId($reviewId)->aggregate()->delete(); + if ($this->getRequest()->isPost()) { + try { + $this->reviewFactory->create()->setId($reviewId)->aggregate()->delete(); - $this->messageManager->addSuccess(__('The review has been deleted.')); - if ($this->getRequest()->getParam('ret') == 'pending') { - $resultRedirect->setPath('review/*/pending'); - } else { - $resultRedirect->setPath('review/*/'); + $this->messageManager->addSuccess(__('The review has been deleted.')); + if ($this->getRequest()->getParam('ret') == 'pending') { + $resultRedirect->setPath('review/*/pending'); + } else { + $resultRedirect->setPath('review/*/'); + } + + return $resultRedirect; + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addError($e->getMessage()); + } catch (\Exception $e) { + $this->messageManager->addException($e, __('Something went wrong deleting this review.')); } - return $resultRedirect; - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); - } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong deleting this review.')); } return $resultRedirect->setPath('review/*/edit/', ['id' => $reviewId]); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php index c792540000233..954c393276c14 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php @@ -13,9 +13,14 @@ class MassDelete extends ProductController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $reviewsIds = $this->getRequest()->getParam('reviews'); if (!is_array($reviewsIds)) { $this->messageManager->addError(__('Please select review(s).')); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php index 2769a35ba9a48..a5850a6896321 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php @@ -13,9 +13,14 @@ class MassUpdateStatus extends ProductController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $reviewsIds = $this->getRequest()->getParam('reviews'); if (!is_array($reviewsIds)) { $this->messageManager->addError(__('Please select review(s).')); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php index eca37d3fe24da..759ec36b9e834 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php @@ -13,9 +13,14 @@ class MassVisibleIn extends ProductController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $reviewsIds = $this->getRequest()->getParam('reviews'); if (!is_array($reviewsIds)) { $this->messageManager->addError(__('Please select review(s).')); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php index 7159b1825dc4d..857f36b19a19c 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php @@ -9,9 +9,14 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; +/** + * Save Review action. + */ class Save extends ProductController { /** + * Save Review action. + * * @return \Magento\Backend\Model\View\Result\Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -63,7 +68,7 @@ public function execute() if ($nextId) { $resultRedirect->setPath('review/*/edit', ['id' => $nextId]); } elseif ($this->getRequest()->getParam('ret') == 'pending') { - $resultRedirect->setPath('*/*/pending'); + $resultRedirect->setPath('review/*/pending'); } else { $resultRedirect->setPath('*/*/'); } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php index 5535c3de26e43..c5610d135222a 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php @@ -5,6 +5,7 @@ */ namespace Magento\Review\Controller\Adminhtml\Rating; +use Magento\Framework\Exception\NotFoundException; use Magento\Review\Controller\Adminhtml\Rating as RatingController; use Magento\Framework\Controller\ResultFactory; @@ -12,19 +13,25 @@ class Delete extends RatingController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - if ($this->getRequest()->getParam('id') > 0) { + $ratingId = (int)$this->getRequest()->getParam('id'); + if ($ratingId) { try { /** @var \Magento\Review\Model\Rating $model */ $model = $this->_objectManager->create(\Magento\Review\Model\Rating::class); - $model->load($this->getRequest()->getParam('id'))->delete(); - $this->messageManager->addSuccess(__('You deleted the rating.')); + $model->load($ratingId)->delete(); + $this->messageManager->addSuccessMessage(__('You deleted the rating.')); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $resultRedirect->setPath('review/rating/edit', ['id' => $this->getRequest()->getParam('id')]); return $resultRedirect; } diff --git a/app/code/Magento/Review/Controller/Product/Post.php b/app/code/Magento/Review/Controller/Product/Post.php index be18f8fe25bbe..67c38f25d7ce3 100644 --- a/app/code/Magento/Review/Controller/Product/Post.php +++ b/app/code/Magento/Review/Controller/Product/Post.php @@ -22,7 +22,7 @@ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - if (!$this->formKeyValidator->validate($this->getRequest())) { + if (!$this->getRequest()->isPost() || !$this->formKeyValidator->validate($this->getRequest())) { $resultRedirect->setUrl($this->_redirect->getRefererUrl()); return $resultRedirect; } diff --git a/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php b/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php index 1526e80f8190a..73e85a7cdc179 100644 --- a/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php +++ b/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php @@ -105,7 +105,8 @@ class PostTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); - $this->request = $this->createPartialMock(\Magento\Framework\App\Request\Http::class, ['getParam']); + $this->request = $this->createPartialMock(\Magento\Framework\App\Request\Http::class, ['getParam', 'isPost']); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->response = $this->createPartialMock(\Magento\Framework\App\Response\Http::class, ['setRedirect']); $this->formKeyValidator = $this->createPartialMock( \Magento\Framework\Data\Form\FormKey\Validator::class, @@ -215,12 +216,12 @@ public function testExecute() $this->reviewSession->expects($this->any())->method('getFormData') ->with(true) ->willReturn($reviewData); - $this->request->expects($this->at(0))->method('getParam') - ->with('category', false) - ->willReturn(false); - $this->request->expects($this->at(1))->method('getParam') - ->with('id') - ->willReturn(1); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + ['category', false, false], + ['id', null, 1], + ] + ); $product = $this->createPartialMock( \Magento\Catalog\Model\Product::class, ['__wakeup', 'isVisibleInCatalog', 'isVisibleInSiteVisibility', 'getId', 'getWebsiteIds'] diff --git a/app/code/Magento/Review/composer.json b/app/code/Magento/Review/composer.json index c1d687c665199..4cc5cfc8d3f03 100644 --- a/app/code/Magento/Review/composer.json +++ b/app/code/Magento/Review/composer.json @@ -18,7 +18,7 @@ "magento/module-review-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Review/etc/acl.xml b/app/code/Magento/Review/etc/acl.xml index 397cc1cce61d6..09b80750da14d 100644 --- a/app/code/Magento/Review/etc/acl.xml +++ b/app/code/Magento/Review/etc/acl.xml @@ -17,7 +17,7 @@ <resource id="Magento_Backend::marketing"> <resource id="Magento_Backend::marketing_user_content"> <resource id="Magento_Review::reviews_all" title="Reviews" translate="title" sortOrder="10"/> - <resource id="Magento_Review::pending" title="Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::pending" title="Pending Reviews" translate="title" sortOrder="20"/> </resource> </resource> </resource> diff --git a/app/code/Magento/Review/etc/adminhtml/menu.xml b/app/code/Magento/Review/etc/adminhtml/menu.xml index e3532483f88af..8b56f36bce68e 100644 --- a/app/code/Magento/Review/etc/adminhtml/menu.xml +++ b/app/code/Magento/Review/etc/adminhtml/menu.xml @@ -9,6 +9,7 @@ <menu> <add id="Magento_Review::catalog_reviews_ratings_ratings" title="Rating" translate="title" module="Magento_Review" sortOrder="60" parent="Magento_Backend::stores_attributes" action="review/rating/" resource="Magento_Review::ratings"/> <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> + <add id="Magento_Review::catalog_reviews_ratings_pending" title="Pending Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="15" action="review/product/pending" resource="Magento_Review::pending"/> <add id="Magento_Review::report_review" title="Reviews" translate="title" module="Magento_Reports" sortOrder="20" parent="Magento_Reports::report" resource="Magento_Reports::review"/> <add id="Magento_Review::report_review_customer" title="By Customers" translate="title" sortOrder="10" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/customer" resource="Magento_Reports::review_customer"/> <add id="Magento_Review::report_review_product" title="By Products" translate="title" sortOrder="20" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/product" resource="Magento_Reports::review_product"/> diff --git a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js index d1c40959e3ec2..88c61fa38af34 100644 --- a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js +++ b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js @@ -20,7 +20,7 @@ define([ showLoader: false, loaderContext: $('.product.data.items') }).done(function (data) { - $('#product-review-container').html(data); + $('#product-review-container').html(data).trigger('contentUpdated'); $('[data-role="product-review"] .pages a').each(function (index, element) { $(element).click(function (event) { //eslint-disable-line max-nested-callbacks processReviews($(element).attr('href'), true); diff --git a/app/code/Magento/Robots/Model/Config/Value.php b/app/code/Magento/Robots/Model/Config/Value.php index 83c21d6602fca..4c80588d814f6 100644 --- a/app/code/Magento/Robots/Model/Config/Value.php +++ b/app/code/Magento/Robots/Model/Config/Value.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Robots\Model\Config; use Magento\Framework\App\Cache\TypeListInterface; @@ -30,12 +31,11 @@ class Value extends ConfigValue implements IdentityInterface const CACHE_TAG = 'robots'; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [self::CACHE_TAG]; /** * @var StoreResolver diff --git a/app/code/Magento/Rule/Block/Editable.php b/app/code/Magento/Rule/Block/Editable.php index 67e4671236ea0..32bcc015e2121 100644 --- a/app/code/Magento/Rule/Block/Editable.php +++ b/app/code/Magento/Rule/Block/Editable.php @@ -9,6 +9,8 @@ use Magento\Framework\View\Element\AbstractBlock; /** + * Renderer for Editable sales rules. + * * @api * @since 100.0.2 */ @@ -52,9 +54,9 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele if ($element->getShowAsText()) { $html = ' <input type="hidden" class="hidden" id="' . - $element->getHtmlId() . + $this->escapeHtmlAttr($element->getHtmlId()) . '" name="' . - $element->getName() . + $this->escapeHtmlAttr($element->getName()) . '" value="' . $element->getValue() . '" data-form-part="' . diff --git a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php index e5d613f7c9558..53ba319d47ef0 100644 --- a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php +++ b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php @@ -137,7 +137,6 @@ public function getDefaultOperatorInputByType() */ $this->_defaultOperatorInputByType['category'] = ['==', '!=', '{}', '!{}', '()', '!()']; $this->_arrayInputTypes[] = 'category'; - $this->_defaultOperatorInputByType['sku'] = ['==', '!=', '{}', '!{}', '()', '!()']; } return $this->_defaultOperatorInputByType; } @@ -383,9 +382,6 @@ public function getInputType() if ($this->getAttributeObject()->getAttributeCode() == 'category_ids') { return 'category'; } - if ($this->getAttributeObject()->getAttributeCode() == 'sku') { - return 'sku'; - } switch ($this->getAttributeObject()->getFrontendInput()) { case 'select': return 'select'; @@ -614,10 +610,6 @@ public function getBindArgumentValue() $this->getValueParsed() )->__toString() ); - } elseif ($this->getAttribute() === 'sku') { - $value = $this->getData('value'); - $value = preg_split('#\s*[,;]\s*#', $value, null, PREG_SPLIT_NO_EMPTY); - $this->setValueParsed($value); } return parent::getBindArgumentValue(); @@ -718,7 +710,7 @@ protected function _getAttributeSetId($productId) public function getOperatorForValidate() { $operator = $this->getOperator(); - if (in_array($this->getInputType(), ['category', 'sku'])) { + if ('category' === $this->getInputType()) { if ($operator == '==') { $operator = '{}'; } elseif ($operator == '!=') { diff --git a/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php b/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php index b3d761b378d94..e0468f17e587a 100644 --- a/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php +++ b/app/code/Magento/Rule/Model/ResourceModel/Rule/Collection/AbstractCollection.php @@ -83,11 +83,21 @@ public function addWebsiteFilter($websiteId) if ($website instanceof \Magento\Store\Model\Website) { $websiteIds[$index] = $website->getId(); } + $websiteIds[$index] = (int) $websiteIds[$index]; } + + $websiteSelect = $this->getConnection()->select(); + $websiteSelect->from( + $this->getTable($entityInfo['associations_table']), + [$entityInfo['rule_id_field']] + )->distinct( + true + )->where( + $this->getConnection()->quoteInto($entityInfo['entity_id_field'] . ' IN (?)', $websiteIds) + ); $this->getSelect()->join( - ['website' => $this->getTable($entityInfo['associations_table'])], - $this->getConnection()->quoteInto('website.' . $entityInfo['entity_id_field'] . ' IN (?)', $websiteIds) - . ' AND main_table.' . $entityInfo['rule_id_field'] . ' = website.' . $entityInfo['rule_id_field'], + ['website' => $websiteSelect], + 'main_table.' . $entityInfo['rule_id_field'] . ' = website.' . $entityInfo['rule_id_field'], [] ); } diff --git a/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php b/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php deleted file mode 100644 index c4e7a591212c5..0000000000000 --- a/app/code/Magento/Rule/Test/Unit/Model/ResourceModel/Rule/Collection/AbstractCollectionTest.php +++ /dev/null @@ -1,200 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Rule\Test\Unit\Model\ResourceModel\Rule\Collection; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -class AbstractCollectionTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $abstractCollection; - - /** - * @var ObjectManagerHelper - */ - protected $objectManagerHelper; - - /** - * @var \Magento\Framework\Data\Collection\EntityFactoryInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_entityFactoryMock; - - /** - * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_loggerMock; - - /** - * @var \Magento\Framework\Data\Collection\Db\FetchStrategyInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_fetchStrategyMock; - - /** - * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_managerMock; - - /** - * @var \Magento\Framework\Model\ResourceModel\Db\AbstractDb|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_db; - - /** - * @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $connectionMock; - - /** - * @var \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject - */ - private $selectMock; - - protected function setUp() - { - $this->_entityFactoryMock = $this->createMock(\Magento\Framework\Data\Collection\EntityFactoryInterface::class); - $this->_loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->_fetchStrategyMock = $this->createMock( - \Magento\Framework\Data\Collection\Db\FetchStrategyInterface::class - ); - $this->_managerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->_db = $this->getMockForAbstractClass( - \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, - [], - '', - false, - false, - true, - ['__sleep', '__wakeup', 'getTable'] - ); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->abstractCollection = $this->getMockForAbstractClass( - \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class, - [ - 'entityFactory' => $this->_entityFactoryMock, - 'logger' => $this->_loggerMock, - 'fetchStrategy' => $this->_fetchStrategyMock, - 'eventManager' => $this->_managerMock, - null, - $this->_db - ], - '', - false, - false, - true, - ['__sleep', '__wakeup', '_getAssociatedEntityInfo', 'getConnection', 'getSelect', 'getTable'] - ); - } - - /** - * @return array - */ - public function addWebsitesToResultDataProvider() - { - return [ - [null, true], - [true, true], - [false, false] - ]; - } - - /** - * @dataProvider addWebsitesToResultDataProvider - */ - public function testAddWebsitesToResult($flag, $expectedResult) - { - $this->abstractCollection->addWebsitesToResult($flag); - $this->assertEquals($expectedResult, $this->abstractCollection->getFlag('add_websites_to_result')); - } - - protected function _prepareAddFilterStubs() - { - $entityInfo = []; - $entityInfo['entity_id_field'] = 'entity_id'; - $entityInfo['rule_id_field'] = 'rule_id'; - $entityInfo['associations_table'] = 'assoc_table'; - - $connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); - $select = $this->createMock(\Magento\Framework\DB\Select::class); - $collectionSelect = $this->createMock(\Magento\Framework\DB\Select::class); - - $connection->expects($this->any()) - ->method('select') - ->will($this->returnValue($select)); - - $select->expects($this->any()) - ->method('from') - ->will($this->returnSelf()); - - $select->expects($this->any()) - ->method('where') - ->will($this->returnSelf()); - - $this->abstractCollection->expects($this->any()) - ->method('getConnection') - ->will($this->returnValue($connection)); - - $this->_db->expects($this->any()) - ->method('getTable') - ->will($this->returnArgument(0)); - - $this->abstractCollection->expects($this->any()) - ->method('getSelect') - ->will($this->returnValue($collectionSelect)); - - $this->abstractCollection->expects($this->any()) - ->method('_getAssociatedEntityInfo') - ->will($this->returnValue($entityInfo)); - } - - public function testAddWebsiteFilter() - { - $this->_prepareAddFilterStubs(); - $website = $this->createPartialMock(\Magento\Store\Model\Website::class, ['getId', '__sleep', '__wakeup']); - - $website->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - - $this->assertInstanceOf( - \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class, - $this->abstractCollection->addWebsiteFilter($website) - ); - } - - public function testAddWebsiteFilterArray() - { - $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->connectionMock->expects($this->atLeastOnce()) - ->method('quoteInto') - ->with($this->equalTo('website. IN (?)'), $this->equalTo(['2', '3'])) - ->willReturn(true); - - $this->abstractCollection->expects($this->atLeastOnce())->method('getSelect')->willReturn($this->selectMock); - $this->abstractCollection->expects($this->atLeastOnce())->method('getConnection') - ->willReturn($this->connectionMock); - - $this->assertInstanceOf( - \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection::class, - $this->abstractCollection->addWebsiteFilter(['2', '3']) - ); - } - - public function testAddFieldToFilter() - { - $this->_prepareAddFilterStubs(); - $result = $this->abstractCollection->addFieldToFilter('website_ids', []); - $this->assertNotNull($result); - } -} diff --git a/app/code/Magento/Rule/composer.json b/app/code/Magento/Rule/composer.json index e37274c19a969..33341dcf1e778 100644 --- a/app/code/Magento/Rule/composer.json +++ b/app/code/Magento/Rule/composer.json @@ -11,7 +11,7 @@ "lib-libxml": "*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create.php index 083055994a282..a26ddc2961765 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create.php @@ -81,7 +81,7 @@ protected function _construct() $this->buttonList->update( 'reset', 'onclick', - 'deleteConfirm(\'' . $confirm . '\', \'' . $this->getCancelUrl() . '\')' + 'deleteConfirm(\'' . $confirm . '\', \'' . $this->getCancelUrl() . '\', {data: {}})' ); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php index d15c218a60b47..c1b67e6dcf88a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php @@ -96,6 +96,11 @@ protected function _prepareLayout() public function getForm() { if ($this->_form === null) { + $storeId = $this->getCreateOrderModel() + ->getSession() + ->getStoreId(); + $this->_storeManager->setCurrentStore($storeId); + $this->_form = $this->_formFactory->create(); $this->_prepareForm(); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php index fe1682e2de830..6625f438f9515 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php @@ -217,11 +217,6 @@ public function getAddressCollectionJson() */ protected function _prepareForm() { - $storeId = $this->getCreateOrderModel() - ->getSession() - ->getStoreId(); - $this->_storeManager->setCurrentStore($storeId); - $fieldset = $this->_form->addFieldset('main', ['no_container' => true]); $addressForm = $this->_customerFormFactory->create('customer_address', 'adminhtml_customer_address'); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php index 4bd2227d4bb1e..3be0df001b1b5 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php @@ -5,12 +5,17 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Search; +use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection + as ProductCollectionDataProvider; +use Magento\Framework\App\ObjectManager; + /** * Adminhtml sales order create search products block * * @api * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { @@ -42,6 +47,11 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended */ protected $_productFactory; + /** + * @var ProductCollectionDataProvider $productCollectionProvider + */ + private $productCollectionProvider; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper @@ -50,6 +60,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended * @param \Magento\Backend\Model\Session\Quote $sessionQuote * @param \Magento\Sales\Model\Config $salesConfig * @param array $data + * @param ProductCollectionDataProvider|null $productCollectionProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -58,12 +69,15 @@ public function __construct( \Magento\Catalog\Model\Config $catalogConfig, \Magento\Backend\Model\Session\Quote $sessionQuote, \Magento\Sales\Model\Config $salesConfig, - array $data = [] + array $data = [], + ProductCollectionDataProvider $productCollectionProvider = null ) { $this->_productFactory = $productFactory; $this->_catalogConfig = $catalogConfig; $this->_sessionQuote = $sessionQuote; $this->_salesConfig = $salesConfig; + $this->productCollectionProvider = $productCollectionProvider + ?: ObjectManager::getInstance()->get(ProductCollectionDataProvider::class); parent::__construct($context, $backendHelper, $data); } @@ -71,6 +85,7 @@ public function __construct( * Constructor * * @return void + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _construct() { @@ -140,20 +155,18 @@ protected function _addColumnFilterToCollection($column) */ protected function _prepareCollection() { + $attributes = $this->_catalogConfig->getProductAttributes(); + $store = $this->getStore(); + /* @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ - $collection = $this->_productFactory->create()->getCollection(); - $collection->setStore( - $this->getStore() - )->addAttributeToSelect( + $collection = $this->productCollectionProvider->getCollectionForStore($store); + $collection->addAttributeToSelect( $attributes - )->addAttributeToSelect( - 'sku' - )->addStoreFilter()->addAttributeToFilter( + ); + $collection->addAttributeToFilter( 'type_id', $this->_salesConfig->getAvailableProductTypes() - )->addAttributeToSelect( - 'gift_message_available' ); $this->setCollection($collection); @@ -248,6 +261,7 @@ public function getGridUrl() * Get selected products * * @return mixed + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _getSelectedProducts() { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php new file mode 100644 index 0000000000000..733791a2f9549 --- /dev/null +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider; + +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Store\Model\Store; + +/** + * Prepares product collection for the grid + */ +class ProductCollection +{ + /** + * @var ProductCollectionFactory + */ + private $collectionFactory; + + /** + * @param ProductCollectionFactory $collectionFactory + */ + public function __construct( + ProductCollectionFactory $collectionFactory + ) { + $this->collectionFactory = $collectionFactory; + } + + /** + * Provide products collection filtered with store + * + * @param Store $store + * @return Collection + */ + public function getCollectionForStore(Store $store):Collection + { + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + + $collection->setStore($store); + $collection->addAttributeToSelect( + 'gift_message_available' + ); + $collection->addAttributeToSelect( + 'sku' + ); + $collection->addStoreFilter(); + + return $collection; + } +} diff --git a/app/code/Magento/Sales/Block/Adminhtml/Totals.php b/app/code/Magento/Sales/Block/Adminhtml/Totals.php index 83b155293c2b9..8172a3c0db4ad 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Totals.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Totals.php @@ -5,6 +5,11 @@ */ namespace Magento\Sales\Block\Adminhtml; +use Magento\Sales\Model\Order; + +/** + * Adminhtml sales totals block + */ class Totals extends \Magento\Sales\Block\Order\Totals { /** @@ -67,12 +72,16 @@ protected function _initTotals() if (!$this->getSource()->getIsVirtual() && ((double)$this->getSource()->getShippingAmount() || $this->getSource()->getShippingDescription()) ) { + $shippingLabel = __('Shipping & Handling'); + if ($this->isFreeShipping($this->getOrder()) && $this->getSource()->getDiscountDescription()) { + $shippingLabel .= sprintf(' (%s)', $this->getSource()->getDiscountDescription()); + } $this->_totals['shipping'] = new \Magento\Framework\DataObject( [ 'code' => 'shipping', 'value' => $this->getSource()->getShippingAmount(), 'base_value' => $this->getSource()->getBaseShippingAmount(), - 'label' => __('Shipping & Handling'), + 'label' => $shippingLabel, ] ); } @@ -109,4 +118,23 @@ protected function _initTotals() return $this; } + + /** + * Availability of free shipping in at least one order item + * + * @param Order $order + * @return bool + */ + private function isFreeShipping(Order $order): bool + { + $isFreeShipping = false; + foreach ($order->getItems() as $orderItem) { + if ($orderItem->getFreeShipping() == '1') { + $isFreeShipping = true; + break; + } + } + + return $isFreeShipping; + } } diff --git a/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php b/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php index b413951d9d4f3..9df515a6c03ba 100644 --- a/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php +++ b/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php @@ -36,9 +36,16 @@ public function decorateAction($value, $row, $column, $isExport) $cell = ''; $state = $row->getState(); if (!empty($state)) { - $url = $this->getUrl('*/*/unassign', ['status' => $row->getStatus(), 'state' => $row->getState()]); + $url = $this->getUrl('*/*/unassign'); $label = __('Unassign'); - $cell = '<a href="' . $url . '">' . $label . '</a>'; + $cell = '<a href="#" data-post="' + .$this->escapeHtmlAttr( + \json_encode([ + 'action' => $url, + 'data' => ['status' => $row->getStatus(), 'state' => $row->getState()] + ]) + ) + .'">' . $label . '</a>'; } return $cell; } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php index de41c3c737968..7e41c7417b38d 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php @@ -24,18 +24,18 @@ public function execute() { $resultRedirect = $this->resultRedirectFactory->create(); if (!$this->isValidPostRequest()) { - $this->messageManager->addError(__('You have not canceled the item.')); + $this->messageManager->addErrorMessage(__('You have not canceled the item.')); return $resultRedirect->setPath('sales/*/'); } $order = $this->_initOrder(); if ($order) { try { $this->orderManagement->cancel($order->getEntityId()); - $this->messageManager->addSuccess(__('You canceled the order.')); + $this->messageManager->addSuccessMessage(__('You canceled the order.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError(__('You have not canceled the item.')); + $this->messageManager->addErrorMessage(__('You have not canceled the item.')); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); } return $resultRedirect->setPath('sales/order/view', ['order_id' => $order->getId()]); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php index 2d22c3343c2e7..efe5ed5b7332a 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php @@ -6,7 +6,7 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Create; -use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Exception\PaymentException; class Save extends \Magento\Sales\Controller\Adminhtml\Order\Create @@ -15,6 +15,7 @@ class Save extends \Magento\Sales\Controller\Adminhtml\Order\Create * Saving quote and create order * * @return \Magento\Framework\Controller\ResultInterface + * @throws NotFoundException * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -23,6 +24,10 @@ public function execute() $path = 'sales/*/'; $pathParams = []; + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + try { // check if the creation of a new customer is allowed if (!$this->_authorization->isAllowed('Magento_Customer::manage') diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php index 826a2a2a8b6c1..f71f567467bff 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php @@ -6,7 +6,7 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; use Magento\Backend\App\Action; -use Magento\Sales\Model\Order; +use Magento\Framework\Exception\NotFoundException; use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; class Save extends \Magento\Backend\App\Action @@ -56,12 +56,17 @@ public function __construct( * We can save only new creditmemo. Existing creditmemos are not editable * * @return \Magento\Backend\Model\View\Result\Redirect|\Magento\Backend\Model\View\Result\Forward + * @throws NotFoundException * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $resultRedirect = $this->resultRedirectFactory->create(); $data = $this->getRequest()->getPost('creditmemo'); if (!empty($data['comment_text'])) { @@ -109,7 +114,7 @@ public function execute() $this->creditmemoSender->send($creditmemo); } - $this->messageManager->addSuccess(__('You created the credit memo.')); + $this->messageManager->addSuccessMessage(__('You created the credit memo.')); $this->_getSession()->getCommentText(true); $resultRedirect->setPath('sales/order/view', ['order_id' => $creditmemo->getOrderId()]); return $resultRedirect; @@ -119,11 +124,11 @@ public function execute() return $resultForward; } } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_getSession()->setFormData($data); } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); - $this->messageManager->addError(__('We can\'t save the credit memo right now.')); + $this->messageManager->addErrorMessage(__('We can\'t save the credit memo right now.')); } $resultRedirect->setPath('sales/*/new', ['_current' => true]); return $resultRedirect; diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php index bfd95666e7c48..e445f98c5aa54 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php @@ -65,6 +65,10 @@ public function __construct( public function execute() { try { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\LocalizedException(__('Invalid request type.')); + } + $this->creditmemoLoader->setOrderId($this->getRequest()->getParam('order_id')); $this->creditmemoLoader->setCreditmemoId($this->getRequest()->getParam('creditmemo_id')); $this->creditmemoLoader->setCreditmemo($this->getRequest()->getParam('creditmemo')); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php index cdb4114f70976..de853bcb6f591 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php @@ -7,7 +7,6 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; use Magento\Framework\Exception\LocalizedException; -use Magento\Backend\App\Action; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\Result\RawFactory; @@ -74,27 +73,27 @@ public function __construct( public function execute() { try { + if (!$this->getRequest()->isPost()) { + throw new LocalizedException(__('Invalid request type.')); + } + $orderId = $this->getRequest()->getParam('order_id'); $invoiceData = $this->getRequest()->getParam('invoice', []); $invoiceItems = isset($invoiceData['items']) ? $invoiceData['items'] : []; /** @var \Magento\Sales\Model\Order $order */ $order = $this->_objectManager->create(\Magento\Sales\Model\Order::class)->load($orderId); if (!$order->getId()) { - throw new \Magento\Framework\Exception\LocalizedException(__('The order no longer exists.')); + throw new LocalizedException(__('The order no longer exists.')); } if (!$order->canInvoice()) { - throw new \Magento\Framework\Exception\LocalizedException( - __('The order does not allow an invoice to be created.') - ); + throw new LocalizedException(__('The order does not allow an invoice to be created.')); } $invoice = $this->invoiceService->prepareInvoice($order, $invoiceItems); if (!$invoice->getTotalQty()) { - throw new \Magento\Framework\Exception\LocalizedException( - __('You can\'t create an invoice without products.') - ); + throw new LocalizedException(__('You can\'t create an invoice without products.')); } $this->registry->register('current_invoice', $invoice); // Save invoice comment text in current invoice object in order to display it in corresponding view diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php index 89820b41a68da..3b98d206d5f66 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php @@ -26,18 +26,18 @@ public function execute() if ($status && $status->getStatus()) { try { $status->assignState($state, $isDefault, $visibleOnFront); - $this->messageManager->addSuccess(__('You assigned the order status.')); + $this->messageManager->addSuccessMessage(__('You assigned the order status.')); return $resultRedirect->setPath('sales/*/'); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while assigning the order status.') ); } } else { - $this->messageManager->addError(__('We can\'t find this order status.')); + $this->messageManager->addErrorMessage(__('We can\'t find this order status.')); } return $resultRedirect->setPath('sales/*/assign'); } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php index 849a7e2d0c817..06fa61c3263de 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php @@ -12,6 +12,7 @@ class Save extends \Magento\Sales\Controller\Adminhtml\Order\Status * Save status form processing * * @return \Magento\Backend\Model\View\Result\Redirect + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { @@ -40,7 +41,9 @@ public function execute() $status = $this->_objectManager->create(\Magento\Sales\Model\Order\Status::class)->load($statusCode); // check if status exist if ($isNew && $status->getStatus()) { - $this->messageManager->addError(__('We found another order status with the same order status code.')); + $this->messageManager->addErrorMessage( + __('We found another order status with the same order status code.') + ); $this->_getSession()->setFormData($data); return $resultRedirect->setPath('sales/*/new'); } @@ -49,12 +52,12 @@ public function execute() try { $status->save(); - $this->messageManager->addSuccess(__('You saved the order status.')); + $this->messageManager->addSuccessMessage(__('You saved the order status.')); return $resultRedirect->setPath('sales/*/'); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + } catch (LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('We can\'t add the order status right now.') ); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php index 04db430e1ffa4..44238725ef65c 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php @@ -6,29 +6,36 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Status; +use Magento\Framework\Exception\NotFoundException; + class Unassign extends \Magento\Sales\Controller\Adminhtml\Order\Status { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $state = $this->getRequest()->getParam('state'); $status = $this->_initStatus(); if ($status) { try { $status->unassignState($state); - $this->messageManager->addSuccess(__('You have unassigned the order status.')); + $this->messageManager->addSuccessMessage(__('You have unassigned the order status.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while unassigning the order.') ); } } else { - $this->messageManager->addError(__('We can\'t find this order status.')); + $this->messageManager->addErrorMessage(__('We can\'t find this order status.')); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); diff --git a/app/code/Magento/Sales/Model/Order/Address/Validator.php b/app/code/Magento/Sales/Model/Order/Address/Validator.php index e6353f7f28899..e970cd66635f9 100644 --- a/app/code/Magento/Sales/Model/Order/Address/Validator.php +++ b/app/code/Magento/Sales/Model/Order/Address/Validator.php @@ -48,8 +48,8 @@ class Validator /** * @param DirectoryHelper $directoryHelper - * @param CountryFactory $countryFactory - * @param EavConfig $eavConfig + * @param CountryFactory $countryFactory + * @param EavConfig $eavConfig */ public function __construct( DirectoryHelper $directoryHelper, @@ -60,6 +60,17 @@ public function __construct( $this->countryFactory = $countryFactory; $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() ->get(EavConfig::class); + } + + /** + * Validate address. + * + * @param \Magento\Sales\Model\Order\Address $address + * @return array + */ + public function validate(Address $address) + { + $warnings = []; if ($this->isTelephoneRequired()) { $this->required['telephone'] = 'Phone Number'; @@ -72,16 +83,7 @@ public function __construct( if ($this->isFaxRequired()) { $this->required['fax'] = 'Fax'; } - } - /** - * - * @param \Magento\Sales\Model\Order\Address $address - * @return array - */ - public function validate(Address $address) - { - $warnings = []; foreach ($this->required as $code => $label) { if (!$address->hasData($code)) { $warnings[] = sprintf('%s is a required field', $label); @@ -194,7 +196,10 @@ protected function isStateRequired($countryId) } /** + * Check whether telephone is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isTelephoneRequired() { @@ -202,7 +207,10 @@ protected function isTelephoneRequired() } /** + * Check whether company is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isCompanyRequired() { @@ -210,7 +218,10 @@ protected function isCompanyRequired() } /** + * Check whether fax is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isFaxRequired() { diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php index ecd5670a319e7..3d2c13cbaaaa9 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php @@ -89,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -96,7 +97,7 @@ public function send( \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $creditmemo->setSendEmail(true); + $creditmemo->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $transport = [ @@ -145,6 +146,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php index 8004483583114..be7fa8296a264 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php @@ -96,10 +96,11 @@ public function __construct( * @param Creditmemo $creditmemo * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Creditmemo $creditmemo, $forceSyncMode = false) { - $creditmemo->setSendEmail(true); + $creditmemo->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $creditmemo->getOrder(); @@ -146,6 +147,7 @@ public function send(Creditmemo $creditmemo, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php index 994fd79945cfd..bd67de7322a62 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php @@ -96,10 +96,11 @@ public function __construct( * @param Invoice $invoice * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Invoice $invoice, $forceSyncMode = false) { - $invoice->setSendEmail(true); + $invoice->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $invoice->getOrder(); @@ -146,6 +147,7 @@ public function send(Invoice $invoice, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php index 6729c746f5565..2b10d25b87a04 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php @@ -96,10 +96,11 @@ public function __construct( * @param Shipment $shipment * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Shipment $shipment, $forceSyncMode = false) { - $shipment->setSendEmail(true); + $shipment->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $shipment->getOrder(); @@ -146,6 +147,7 @@ public function send(Shipment $shipment, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php index aa0687bee504f..5ae3306ddf75b 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php @@ -89,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -96,7 +97,7 @@ public function send( \Magento\Sales\Api\Data\InvoiceCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $invoice->setSendEmail(true); + $invoice->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $transport = [ @@ -145,6 +146,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php index 0a393548069f5..3657f84d4445d 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php @@ -89,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -96,7 +97,7 @@ public function send( \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $shipment->setSendEmail(true); + $shipment->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $transport = [ @@ -145,6 +146,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Status/History.php b/app/code/Magento/Sales/Model/Order/Status/History.php index 37aef2a5a29aa..d563f9f15b94a 100644 --- a/app/code/Magento/Sales/Model/Order/Status/History.php +++ b/app/code/Magento/Sales/Model/Order/Status/History.php @@ -142,7 +142,7 @@ public function getOrder() */ public function getStatusLabel() { - if ($this->getOrder()) { + if ($this->getOrder() && $this->getStatus() !== null) { return $this->getOrder()->getConfig()->getStatusLabel($this->getStatus()); } return null; diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php index f18118447f95f..820ef71a7ea79 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php @@ -9,7 +9,7 @@ use Magento\Sales\Model\Order; /** - * Class to check order State. + * Class to check and adjust order state/status. */ class State { @@ -31,7 +31,10 @@ public function check(Order $order) } if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice()) { - if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE]) && !$order->canCreditmemo()) { + if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE]) + && !$order->canCreditmemo() + && !$order->canShip() + ) { $order->setState(Order::STATE_CLOSED) ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED)); } elseif ($currentState === Order::STATE_PROCESSING && !$order->canShip()) { diff --git a/app/code/Magento/Sales/Setup/UpgradeData.php b/app/code/Magento/Sales/Setup/UpgradeData.php index 77b96791e8cea..2e5a454e62fdd 100644 --- a/app/code/Magento/Sales/Setup/UpgradeData.php +++ b/app/code/Magento/Sales/Setup/UpgradeData.php @@ -15,6 +15,7 @@ use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\UpgradeDataInterface; use Magento\Quote\Model\QuoteFactory; +use Magento\Sales\Model\Order\Address; use Magento\Sales\Model\OrderFactory; use Magento\Sales\Model\ResourceModel\Order\Address\CollectionFactory as AddressCollectionFactory; @@ -42,27 +43,14 @@ class UpgradeData implements UpgradeDataInterface */ private $aggregatedFieldConverter; - /** - * @var AddressCollectionFactory - */ - private $addressCollectionFactory; - - /** - * @var OrderFactory - */ - private $orderFactory; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - /** * @var State */ private $state; /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * * @param SalesSetupFactory $salesSetupFactory * @param Config $eavConfig * @param AggregatedFieldDataConverter $aggregatedFieldConverter @@ -83,9 +71,6 @@ public function __construct( $this->salesSetupFactory = $salesSetupFactory; $this->eavConfig = $eavConfig; $this->aggregatedFieldConverter = $aggregatedFieldConverter; - $this->addressCollectionFactory = $addressCollFactory; - $this->orderFactory = $orderFactory; - $this->quoteFactory = $quoteFactory; $this->state = $state; } @@ -125,6 +110,7 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface * @param string $setupVersion * @param SalesSetup $salesSetup * @return void + * @throws \Magento\Framework\DB\FieldDataConversionException */ private function convertSerializedDataToJson($setupVersion, SalesSetup $salesSetup) { @@ -173,32 +159,95 @@ private function convertSerializedDataToJson($setupVersion, SalesSetup $salesSet /** * Fill quote_address_id in table sales_order_address if it is empty. - * * @param ModuleDataSetupInterface $setup */ public function fillQuoteAddressIdInSalesOrderAddress(ModuleDataSetupInterface $setup) { - $addressTable = $setup->getTable('sales_order_address'); - $updateOrderAddress = $setup->getConnection() + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_SHIPPING); + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_BILLING); + } + + /** + * @param ModuleDataSetupInterface $setup + * @param string $addressType + */ + private function fillQuoteAddressIdInSalesOrderAddressByType(ModuleDataSetupInterface $setup, $addressType) + { + $salesConnection = $setup->getConnection('sales'); + + $orderTable = $setup->getTable('sales_order', 'sales'); + $orderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $salesConnection ->select() + ->from( + ['sales_order_address' => $orderAddressTable], + ['entity_id', 'address_type'] + ) ->joinInner( - ['sales_order' => $setup->getTable('sales_order')], - $addressTable . '.parent_id = sales_order.entity_id', - ['quote_address_id' => 'quote_address.address_id'] + ['sales_order' => $orderTable], + 'sales_order_address.parent_id = sales_order.entity_id', + ['quote_id' => 'sales_order.quote_id'] + ) + ->where('sales_order_address.quote_address_id IS NULL') + ->where('sales_order_address.address_type = ?', $addressType) + ->order('sales_order_address.entity_id'); + + $batchSize = 5000; + $result = $salesConnection->query($query); + $count = $result->rowCount(); + $batches = ceil($count / $batchSize); + + for ($batch = $batches; $batch > 0; $batch--) { + $query->limitPage($batch, $batchSize); + $result = $salesConnection->fetchAssoc($query); + + $this->fillQuoteAddressIdInSalesOrderAddressProcessBatch($setup, $result, $addressType); + } + } + + /** + * @param ModuleDataSetupInterface $setup + * @param array $orderAddresses + * @param string $addressType + */ + private function fillQuoteAddressIdInSalesOrderAddressProcessBatch( + ModuleDataSetupInterface $setup, + array $orderAddresses, + $addressType + ) { + $salesConnection = $setup->getConnection('sales'); + $quoteConnection = $setup->getConnection('checkout'); + + $quoteAddressTable = $setup->getTable('quote_address', 'checkout'); + $quoteTable = $setup->getTable('quote', 'checkout'); + $salesOrderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $quoteConnection + ->select() + ->from( + ['quote_address' => $quoteAddressTable], + ['quote_id', 'address_id'] ) ->joinInner( - ['quote_address' => $setup->getTable('quote_address')], - 'sales_order.quote_id = quote_address.quote_id - AND ' . $addressTable . '.address_type = quote_address.address_type', + ['quote' => $quoteTable], + 'quote_address.quote_id = quote.entity_id', [] ) - ->where( - $addressTable . '.quote_address_id IS NULL' - ); - $updateOrderAddress = $setup->getConnection()->updateFromSelect( - $updateOrderAddress, - $addressTable - ); - $setup->getConnection()->query($updateOrderAddress); + ->where('quote.entity_id in (?)', array_column($orderAddresses, 'quote_id')) + ->where('address_type = ?', $addressType); + + $quoteAddresses = $quoteConnection->fetchAssoc($query); + + foreach ($orderAddresses as $orderAddress) { + $bind = [ + 'quote_address_id' => $quoteAddresses[$orderAddress['quote_id']]['address_id'] ?? null, + ]; + $where = [ + 'entity_id = ?' => $orderAddress['entity_id'] + ]; + + $salesConnection->update($salesOrderAddressTable, $bind, $where); + } } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index 479b631db0b71..15d1de779f87e 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -92,6 +92,7 @@ <argument name="product" defaultValue="_defaultProduct"/> <argument name="quantity" type="string" defaultValue="1"/> </arguments> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitForAddProductVisible"/> <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilter"/> <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearch"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml new file mode 100644 index 0000000000000..5f0a5078734d7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontFreeShippingRecalculationAfterCouponCodeAddedTest"> + <annotations> + <features value="Sales"/> + <title value="Checkout Free Shipping Recalculation after Coupon Code Applied"/> + <stories value="Checkout Free Shipping Recalculation after Coupon Code Applied"/> + <description value="User should be able to do checkout free shipping recalculation after adding coupon code"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15412"/> + <useCaseId value="MAGETWO-96375"/> + <group value="sales"/> + <group value="salesRule"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <field key="price">90</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"/> + + <!-- Enable Free Shipping and set minimum order amount --> + <createData entity="FreeShippingMethodWithMinimumOrderAmount90" stepKey="enableFreeShippingAndSetMinimumOrderAmount"/> + + <!-- Create Cart Price Rule --> + <createData entity="SalesRuleSpecificCouponWithPercentDiscount" stepKey="createCartPriceRule"> + <field key="simple_free_shipping">0</field> + </createData> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginToStorefront"> + <argument name="customer" value="$$createSimpleUsCustomer$$"/> + </actionGroup> + </before> + + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <createData entity="ResetFreeShippingMethodWithMinimumOrderAmount90" stepKey="resetFreeShippingMethodAndMinimumOrderAmount"/> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutFromStorefront"/> + </after> + + <!-- Add product to Shopping Cart and apply coupon --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="navigateToProductPage"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartRule"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="$$createCouponForCartPriceRule.code$$"/> + </actionGroup> + + <!-- Proceed to Checkout and make sure Free Shipping method isn't displaying --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <dontSeeElement selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}" stepKey="dontSeeFreeShipping"/> + + <!-- Back to Shopping Cart page and cancel coupon--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> + <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="cancelCoupon"/> + + <!-- Proceed to Checkout, select Free Shipping method and apply coupon --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> + <actionGroup ref="CheckoutSelectShippingMethodActionGroup" stepKey="selectFreeShipping"> + <argument name="shippingMethod" value="Free"/> + </actionGroup> + <actionGroup ref="StorefrontApplyCouponOnCheckoutActionGroup" stepKey="applyCouponOnCheckout"> + <argument name="couponCode" value="$$createCouponForCartPriceRule.code$$"/> + <argument name="successMessage" value="Your coupon was successfully applied."/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + + <!-- Try to Place Order --> + <actionGroup ref="AssertStorefrontErrorMessageOnOrderSubmit" stepKey="tryToPlaceOrder"> + <argument name="errorMessage" value="The shipping method is missing. Select the shipping method and try again."/> + </actionGroup> + + <!-- Go back to Shipping step and select Shipping method --> + <amOnPage url="{{CheckoutPage.url}}/#shipping" stepKey="navigateToShippingStep"/> + <actionGroup ref="CheckoutSelectShippingMethodActionGroup" stepKey="selectFlatRateShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + + <!-- Select Payment method and Place order--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod1"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/CancelTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/CancelTest.php index 363f78e738f12..d37b0f6121b37 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/CancelTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/CancelTest.php @@ -74,10 +74,12 @@ protected function setUp() ['setRedirect', 'sendResponse'] ); $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); + ->disableOriginalConstructor() + ->getMock(); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->messageManager = $this->createPartialMock( \Magento\Framework\Message\Manager::class, - ['addSuccess', 'addError'] + ['addSuccessMessage', 'addErrorMessage'] ); $this->orderRepositoryMock = $this->getMockBuilder(\Magento\Sales\Api\OrderRepositoryInterface::class) ->disableOriginalConstructor() @@ -101,8 +103,6 @@ protected function setUp() \Magento\Sales\Controller\Adminhtml\Order\Cancel::class, [ 'context' => $this->context, - 'request' => $this->request, - 'response' => $this->response, 'orderRepository' => $this->orderRepositoryMock ] ); @@ -117,7 +117,7 @@ public function testExecuteNotPost() ->method('isPost') ->willReturn(false); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('You have not canceled the item.'); $this->resultRedirect->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php index cc2bf929f8250..31e5318aba409 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php @@ -77,6 +77,7 @@ protected function setUp() $this->_responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); $this->_responseMock->headersSentThrowsException = false; $this->_requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->_requestMock->expects($this->any())->method('isPost')->willReturn(true); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $constructArguments = $objectManager->getConstructArguments(\Magento\Backend\Model\Session::class, ['storage' => new \Magento\Framework\Session\Storage()] @@ -230,7 +231,7 @@ public function testSaveActionWithNegativeCreditmemo() */ protected function _setSaveActionExpectationForMageCoreException($data, $errorMessage) { - $this->_messageManager->expects($this->once())->method('addError')->with($this->equalTo($errorMessage)); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($this->equalTo($errorMessage)); $this->_sessionMock->expects($this->once())->method('setFormData')->with($this->equalTo($data)); } } diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/UpdateQtyTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/UpdateQtyTest.php index e2554eefb9b4e..47c6aea844aca 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/UpdateQtyTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/UpdateQtyTest.php @@ -115,6 +115,7 @@ protected function setUp() $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor() ->getMock(); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php index 899e3defc19a8..90374a2597a02 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php @@ -89,8 +89,8 @@ protected function setUp() ->getMock(); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor() - ->setMethods([]) ->getMock(); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http::class) ->disableOriginalConstructor() ->getMock(); @@ -191,14 +191,13 @@ public function testExecute() $invoiceData = ['comment_text' => 'test']; $response = 'test data'; - $this->requestMock->expects($this->at(0)) - ->method('getParam') - ->with('order_id') - ->will($this->returnValue($orderId)); - $this->requestMock->expects($this->at(1)) - ->method('getParam') - ->with('invoice', []) - ->will($this->returnValue($invoiceData)); + $this->requestMock->expects($this->any())->method('getParam') + ->willReturnMap( + [ + ['order_id', null, $orderId], + ['invoice', [], $invoiceData], + ] + ); $invoiceMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Invoice::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php index 9fd2a8b0d929f..859fbde31f5d8 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -249,7 +249,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -279,7 +279,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setTemplateVars') ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php index 31bf846689230..02a2bbec72389 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php @@ -90,7 +90,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -130,7 +130,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -197,6 +197,8 @@ public function sendDataProvider() * @param bool $isVirtualOrder * @param int $formatCallCount * @param string|null $expectedShippingAddress + * + * @return void * @dataProvider sendVirtualOrderDataProvider */ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expectedShippingAddress) @@ -207,7 +209,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -242,7 +244,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php index 9c54c716e4207..ba2f1166baf3c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -90,7 +90,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -136,7 +136,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -212,7 +212,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -247,7 +247,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php index b1b18af63b590..8a71c738e9fbe 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php @@ -90,7 +90,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -136,7 +136,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -212,7 +212,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -247,7 +247,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); $this->shipmentResourceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php index 8a4e2920ba207..dcf689cf7d53b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php @@ -247,7 +247,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -277,7 +277,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setTemplateVars') ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php index 94347e8b32d54..391e99ba6f835 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -249,7 +249,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -279,7 +279,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setTemplateVars') ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Status/HistoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Status/HistoryTest.php index e7187219d56b6..866fef378a1e9 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Status/HistoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Status/HistoryTest.php @@ -83,6 +83,17 @@ public function testGetStatusLabel() $this->assertEquals($status, $this->model->getStatusLabel()); } + /** + * @return void + */ + public function testGetStatusLabelWithNullStatus() + { + $this->model->setOrder($this->order); + $this->model->setStatus(null); + + $this->assertNull($this->model->getStatusLabel()); + } + public function testGetStoreFromStoreManager() { $resultStore = 1; diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php index 48f4a282a2be2..65313c584454b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php @@ -118,13 +118,13 @@ public function stateCheckDataProvider(): array { return [ 'processing - !canCreditmemo!canShip -> closed' => - [false, 1, false, 0, Order::STATE_PROCESSING, Order::STATE_CLOSED], + [false, 1, false, 1, Order::STATE_PROCESSING, Order::STATE_CLOSED], 'complete - !canCreditmemo,!canShip -> closed' => - [false, 1, false, 0, Order::STATE_COMPLETE, Order::STATE_CLOSED], - 'processing - !canCreditmemo,canShip -> closed' => - [false, 1, true, 0, Order::STATE_PROCESSING, Order::STATE_CLOSED], - 'complete - !canCreditmemo,canShip -> closed' => - [false, 1, true, 0, Order::STATE_COMPLETE, Order::STATE_CLOSED], + [false, 1, false, 1, Order::STATE_COMPLETE, Order::STATE_CLOSED], + 'processing - !canCreditmemo,canShip -> processing' => + [false, 1, true, 2, Order::STATE_PROCESSING, Order::STATE_PROCESSING], + 'complete - !canCreditmemo,canShip -> complete' => + [false, 1, true, 1, Order::STATE_COMPLETE, Order::STATE_COMPLETE], 'processing - canCreditmemo,!canShip -> complete' => [true, 1, false, 1, Order::STATE_PROCESSING, Order::STATE_COMPLETE], 'complete - canCreditmemo,!canShip -> complete' => diff --git a/app/code/Magento/Sales/composer.json b/app/code/Magento/Sales/composer.json index ed5f939d81869..a2730b255639f 100644 --- a/app/code/Magento/Sales/composer.json +++ b/app/code/Magento/Sales/composer.json @@ -33,7 +33,7 @@ "magento/module-sales-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.6", + "version": "101.0.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Sales/etc/adminhtml/system.xml b/app/code/Magento/Sales/etc/adminhtml/system.xml index 2dc467d6ca247..e437918b683b2 100644 --- a/app/code/Magento/Sales/etc/adminhtml/system.xml +++ b/app/code/Magento/Sales/etc/adminhtml/system.xml @@ -27,18 +27,23 @@ <label>Checkout Totals Sort Order</label> <field id="discount" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Discount</label> + <validate>required-number validate-number</validate> </field> <field id="grand_total" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Grand Total</label> + <validate>required-number validate-number</validate> </field> <field id="shipping" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Shipping</label> + <validate>required-number validate-number</validate> </field> <field id="subtotal" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Subtotal</label> + <validate>required-number validate-number</validate> </field> <field id="tax" translate="label" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Tax</label> + <validate>required-number validate-number</validate> </field> </group> <group id="reorder" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index b4c1e63902121..6311ed60dafe7 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -1009,4 +1009,9 @@ <preference for="Magento\Sales\Api\OrderCustomerDelegateInterface" type="Magento\Sales\Model\Order\OrderCustomerDelegate" /> + <type name="Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityChecker"> + <arguments> + <argument name="productAvailabilityChecks" xsi:type="array" /> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml index 92139896273da..643146f7bb5cb 100755 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml @@ -33,7 +33,6 @@ $taxAmount = $block->getTotal()->getValue(); <?php $percent = $info['percent']; ?> <?php $amount = $info['amount']; ?> <?php $rates = $info['rates']; ?> - <?php $isFirst = 1; ?> <?php foreach ($rates as $rate): ?> <tr class="summary-details-<?= /* @escapeNotVerified */ $taxIter ?> summary-details<?php if ($isTop): echo ' summary-details-first'; endif; ?>" style="display:none;"> @@ -44,13 +43,10 @@ $taxAmount = $block->getTotal()->getValue(); <?php endif; ?> <br /> </td> - <?php if ($isFirst): ?> - <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-amount" rowspan="<?= count($rates) ?>"> - <?= /* @escapeNotVerified */ $block->formatPrice($amount) ?> - </td> - <?php endif; ?> + <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-amount"> + <?= /* @escapeNotVerified */ $block->formatPrice(($amount*(float)$rate['percent'])/$percent) ?> + </td> </tr> - <?php $isFirst = 0; ?> <?php $isTop = 0; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new.html b/app/code/Magento/Sales/view/frontend/email/shipment_new.html index 8af49f322c682..84f5acb29ea3b 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new.html @@ -53,7 +53,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html index df1677f56a500..bb181126724da 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html @@ -51,7 +51,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml b/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml new file mode 100644 index 0000000000000..91414663951d3 --- /dev/null +++ b/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <update handle="sales_email_order_shipment_renderers"/> + <body> + <block class="Magento\Framework\View\Element\Template" name="sales.order.email.shipment.track" template="Magento_Sales::email/shipment/track.phtml"/> + </body> +</page> \ No newline at end of file diff --git a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php index 0cb286056d825..bee7573c1fe2a 100644 --- a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php +++ b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php @@ -26,7 +26,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to delete this?' - ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\')', + ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/CouponsMassDelete.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/CouponsMassDelete.php index dcbc290f98579..a55d4c86cfef7 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/CouponsMassDelete.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/CouponsMassDelete.php @@ -12,9 +12,14 @@ class CouponsMassDelete extends \Magento\SalesRule\Controller\Adminhtml\Promo\Qu * Coupons mass delete action * * @return void + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.')); + } + $this->_initRule(); $rule = $this->_coreRegistry->registry(\Magento\SalesRule\Model\RegistryConstants::CURRENT_SALES_RULE); diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php index 9adb62583985d..b505fd1d6a0fb 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php @@ -6,28 +6,35 @@ */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; +use Magento\Framework\Exception\NotFoundException; + class Delete extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote { /** * Delete promo quote action * * @return void + * @throws NotFoundException */ public function execute() { - $id = $this->getRequest()->getParam('id'); + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + + $id = (int)$this->getRequest()->getParam('id'); if ($id) { try { $model = $this->_objectManager->create(\Magento\SalesRule\Model\Rule::class); $model->load($id); $model->delete(); - $this->messageManager->addSuccess(__('You deleted the rule.')); + $this->messageManager->addSuccessMessage(__('You deleted the rule.')); $this->_redirect('sales_rule/*/'); return; } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('We can\'t delete the rule right now. Please review the log and try again.') ); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); @@ -35,7 +42,7 @@ public function execute() return; } } - $this->messageManager->addError(__('We can\'t find a rule to delete.')); + $this->messageManager->addErrorMessage(__('We can\'t find a rule to delete.')); $this->_redirect('sales_rule/*/'); } } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php index 5e6f3847c8e31..423cd1543117b 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php @@ -9,6 +9,9 @@ use Magento\Framework\DB\Select; use Magento\Framework\Serialize\Serializer\Json; use Magento\Quote\Model\Quote\Address; +use Magento\SalesRule\Api\Data\CouponInterface; +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\Rule; /** * Sales Rules resource collection model. @@ -107,12 +110,15 @@ protected function mapAssociatedEntities($entityType, $objectField) $associatedEntities = $this->getConnection()->fetchAll($select); - array_map(function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { - $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); - $itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField); - $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; - $item->setData($objectField, $itemAssociatedValue); - }, $associatedEntities); + array_map( + function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { + $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); + $itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField); + $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; + $item->setData($objectField, $itemAssociatedValue); + }, + $associatedEntities + ); } /** @@ -141,6 +147,7 @@ protected function _afterLoad() * @param string $couponCode * @param string|null $now * @param Address $address allow extensions to further filter out rules based on quote address + * @throws \Zend_Db_Select_Exception * @use $this->addWebsiteGroupDateFilter() * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return $this @@ -153,32 +160,24 @@ public function setValidationFilter( Address $address = null ) { if (!$this->getFlag('validation_filter')) { - /* We need to overwrite joinLeft if coupon is applied */ - $this->getSelect()->reset(); - parent::_initSelect(); + $this->prepareSelect($websiteId, $customerGroupId, $now); - $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); - $select = $this->getSelect(); + $noCouponRules = $this->getNoCouponCodeSelect(); - $connection = $this->getConnection(); - if (strlen($couponCode)) { - $noCouponWhereCondition = $connection->quoteInto( - 'main_table.coupon_type = ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); - $relatedRulesIds = $this->getCouponRelatedRuleIds($couponCode); - - $select->where( - $noCouponWhereCondition . ' OR main_table.rule_id IN (?)', - $relatedRulesIds, - Select::TYPE_CONDITION - ); + if ($couponCode) { + $couponRules = $this->getCouponCodeSelect($couponCode); + + $allAllowedRules = $this->getConnection()->select(); + $allAllowedRules->union([$noCouponRules, $couponRules], Select::SQL_UNION_ALL); + + $wrapper = $this->getConnection()->select(); + $wrapper->from($allAllowedRules); + + $this->_select = $wrapper; } else { - $this->addFieldToFilter( - 'main_table.coupon_type', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); + $this->_select = $noCouponRules; } + $this->setOrder('sort_order', self::SORT_ORDER_ASC); $this->setFlag('validation_filter', true); } @@ -187,72 +186,98 @@ public function setValidationFilter( } /** - * Get rules ids related to coupon code + * Recreate the default select object for specific needs of salesrule evaluation with coupon codes. * - * @param string $couponCode - * @return array + * @param int $websiteId + * @param int $customerGroupId + * @param string $now */ - private function getCouponRelatedRuleIds(string $couponCode): array + private function prepareSelect($websiteId, $customerGroupId, $now) { - $connection = $this->getConnection(); - $select = $connection->select()->from( - ['main_table' => $this->getTable('salesrule')], - 'rule_id' + $this->getSelect()->reset(); + parent::_initSelect(); + + $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); + } + + /** + * Return select object to determine all active rules not needing a coupon code. + * + * @return Select + */ + private function getNoCouponCodeSelect() + { + $noCouponSelect = clone $this->getSelect(); + + $noCouponSelect->where( + 'main_table.coupon_type = ?', + Rule::COUPON_TYPE_NO_COUPON ); - $select->joinLeft( - ['rule_coupons' => $this->getTable('salesrule_coupon')], - $connection->quoteInto( - 'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, - null - ) + + $noCouponSelect->columns([Coupon::KEY_CODE => new \Zend_Db_Expr('NULL')]); + + return $noCouponSelect; + } + + /** + * Determine all active rules that are valid for the given coupon code. + * + * @param string $couponCode + * @return Select + */ + private function getCouponCodeSelect($couponCode) + { + $couponSelect = clone $this->getSelect(); + + $this->joinCouponTable($couponCode, $couponSelect); + + $notExpired = $this->getConnection()->quoteInto( + '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', + $this->_date->date()->format('Y-m-d') ); - $autoGeneratedCouponCondition = [ - $connection->quoteInto( - "main_table.coupon_type = ?", - \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO - ), - $connection->quoteInto( - "rule_coupons.type = ?", - \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED - ), - ]; - - $orWhereConditions = [ - "(" . implode($autoGeneratedCouponCondition, " AND ") . ")", - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - ]; - - $andWhereConditions = [ - $connection->quoteInto( - 'rule_coupons.code = ?', - $couponCode - ), - $connection->quoteInto( - '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', - $this->_date->date()->format('Y-m-d') - ), - ]; - - $orWhereCondition = implode(' OR ', $orWhereConditions); - $andWhereCondition = implode(' AND ', $andWhereConditions); - - $select->where( - '(' . $orWhereCondition . ') AND ' . $andWhereCondition, + $isAutogenerated = + $this->getConnection()->quoteInto('main_table.coupon_type = ?', Rule::COUPON_TYPE_AUTO) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.type = ?', CouponInterface::TYPE_GENERATED); + + $isValidSpecific = + $this->getConnection()->quoteInto('(main_table.coupon_type = ?)', Rule::COUPON_TYPE_SPECIFIC) + . ' AND (' . + '(main_table.use_auto_generation = 1 AND rule_coupons.type = 1)' + . ' OR ' . + '(main_table.use_auto_generation = 0 AND rule_coupons.type = 0)' + . ')'; + + $couponSelect->where( + "$notExpired AND ($isAutogenerated OR $isValidSpecific)", null, Select::TYPE_CONDITION ); - $select->group('main_table.rule_id'); - return $connection->fetchCol($select); + return $couponSelect; + } + + /** + * Join coupon table to select. + * + * @param string $couponCode + * @param Select $couponSelect + */ + private function joinCouponTable($couponCode, Select $couponSelect) + { + $couponJoinCondition = + 'main_table.rule_id = rule_coupons.rule_id' + . ' AND ' . + $this->getConnection()->quoteInto('main_table.coupon_type <> ?', Rule::COUPON_TYPE_NO_COUPON) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.code = ?', $couponCode); + + $couponSelect->joinInner( + ['rule_coupons' => $this->getTable('salesrule_coupon')], + $couponJoinCondition, + [Coupon::KEY_CODE] + ); } /** diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index fd5953697c7db..96867222d170a 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -58,6 +58,7 @@ public function __construct( public function loadAttributeOptions() { $attributes = [ + 'base_subtotal_with_discount' => __('Subtotal (Excl. Tax)'), 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), diff --git a/app/code/Magento/SalesRule/Model/Utility.php b/app/code/Magento/SalesRule/Model/Utility.php index a3876a9d7e046..45c5dc3da0ebd 100644 --- a/app/code/Magento/SalesRule/Model/Utility.php +++ b/app/code/Magento/SalesRule/Model/Utility.php @@ -189,6 +189,8 @@ public function deltaRoundingFix( ) { $discountAmount = $discountData->getAmount(); $baseDiscountAmount = $discountData->getBaseAmount(); + $rowTotalInclTax = $item->getRowTotalInclTax(); + $baseRowTotalInclTax = $item->getBaseRowTotalInclTax(); //TODO Seems \Magento\Quote\Model\Quote\Item\AbstractItem::getDiscountPercent() returns float value //that can not be used as array index @@ -205,6 +207,23 @@ public function deltaRoundingFix( - $this->priceCurrency->round($baseDiscountAmount); } + /** + * When we have 100% discount check if totals will not be negative + */ + + if ($percentKey == 100) { + $discountDelta = $rowTotalInclTax - $discountAmount; + $baseDiscountDelta = $baseRowTotalInclTax - $baseDiscountAmount; + + if ($discountDelta < 0) { + $discountAmount += $discountDelta; + } + + if ($baseDiscountDelta < 0) { + $baseDiscountAmount += $baseDiscountDelta; + } + } + $discountData->setAmount($this->priceCurrency->round($discountAmount)); $discountData->setBaseAmount($this->priceCurrency->round($baseDiscountAmount)); diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml index 0ea7ec06ca869..4cd0637e83b77 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml @@ -28,16 +28,18 @@ <arguments> <argument name="couponCode" type="string"/> </arguments> - <waitForElement selector="{{AdminCartPriceRuleDiscountSection.discountTab}}" time="30" stepKey="waitForCouponHeader" /> - <conditionalClick selector="{{AdminCartPriceRuleDiscountSection.discountTab}}" dependentSelector="{{AdminCartPriceRuleDiscountSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader" /> - <waitForElementVisible selector="{{AdminCartPriceRuleDiscountSection.couponInput}}" stepKey="waitForCouponField" /> - <fillField userInput="{{couponCode}}" selector="{{AdminCartPriceRuleDiscountSection.couponInput}}" stepKey="fillCouponField"/> - <click selector="{{AdminCartPriceRuleDiscountSection.applyCodeBtn}}" stepKey="clickApplyButton"/> + <waitForElementVisible selector="{{StorefrontDiscountSection.discountTab}}" time="30" stepKey="waitForCouponHeader" /> + <conditionalClick selector="{{StorefrontDiscountSection.discountTab}}" dependentSelector="{{StorefrontDiscountSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader" /> + <waitForElementVisible selector="{{StorefrontDiscountSection.couponInput}}" stepKey="waitForCouponField" /> + <fillField userInput="{{couponCode}}" selector="{{StorefrontDiscountSection.couponInput}}" stepKey="fillCouponField"/> + <click selector="{{StorefrontDiscountSection.applyCodeBtn}}" stepKey="clickApplyButton"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitForSuccessMessage" /> <see userInput='You used coupon code "{{couponCode}}".' selector="{{StorefrontMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> </actionGroup> <!-- Apply Sales Rule Coupon to the cart --> <actionGroup name="StorefrontTryingToApplyCouponActionGroup" extends="StorefrontApplyCouponActionGroup"> + <remove keyForRemoval="waitForSuccessMessage"/> <remove keyForRemoval="seeSuccessMessage"/> <waitForElementVisible selector="{{StorefrontMessagesSection.error}}" stepKey="waitError"/> <see selector="{{StorefrontMessagesSection.error}}" userInput='The coupon code "{{couponCode}}" is not valid.' @@ -46,10 +48,21 @@ <!-- Cancel Sales Rule Coupon applied to the cart --> <actionGroup name="StorefrontCancelCouponActionGroup"> - <waitForElement selector="{{AdminCartPriceRuleDiscountSection.discountTab}}" time="30" stepKey="waitForCouponHeader" /> - <conditionalClick selector="{{AdminCartPriceRuleDiscountSection.discountTab}}" dependentSelector="{{AdminCartPriceRuleDiscountSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader" /> - <waitForElementVisible selector="{{AdminCartPriceRuleDiscountSection.couponInput}}" stepKey="waitForCouponField" /> - <click selector="{{AdminCartPriceRuleDiscountSection.cancelButton}}" stepKey="clickCancelButton"/> + <waitForElementVisible selector="{{StorefrontDiscountSection.discountTab}}" time="30" stepKey="waitForCouponHeader" /> + <conditionalClick selector="{{StorefrontDiscountSection.discountTab}}" dependentSelector="{{AdminCartPriceRuleDiscountSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader" /> + <waitForElementVisible selector="{{StorefrontDiscountSection.couponInput}}" stepKey="waitForCouponField" /> + <click selector="{{StorefrontDiscountSection.cancelCoupon}}" stepKey="clickCancelButton"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> <see userInput="You canceled the coupon code." selector="{{StorefrontMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> </actionGroup> + + <actionGroup name="StorefrontApplyCouponOnCheckoutActionGroup" extends="StorefrontApplyCouponActionGroup"> + <arguments> + <argument name="successMessage" type="string"/> + </arguments> + <waitForElementVisible selector="{{StorefrontDiscountSection.discountInput}}" stepKey="waitForCouponField"/> + <fillField userInput="{{couponCode}}" selector="{{StorefrontDiscountSection.discountInput}}" stepKey="fillCouponField"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.successMessage}}" stepKey="waitForSuccessMessage" /> + <see userInput='{{successMessage}}' selector="{{StorefrontMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml index 2ae50489b6d12..582e50cc766e0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontDiscountSection.xml @@ -6,10 +6,13 @@ */ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontDiscountSection"> - <element name="discountTab" type="button" selector="#block-discount"/> + <element name="discountTab" type="button" selector="#block-discount-heading"/> <element name="couponInput" type="input" selector="#coupon_code"/> - <element name="applyCodeBtn" type="button" selector="//span[text()='Apply Discount']"/> + <element name="applyCodeBtn" type="button" selector="button[value='Apply Discount']"/> + <element name="cancelCoupon" type="button" selector="button[value='Cancel Coupon']"/> + <element name="discountInput" type="input" selector="#discount-code"/> + <element name="discountBlockActive" type="text" selector=".block.discount.active"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php b/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php index fb01476ed6b34..a19de4ff2ef90 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php @@ -62,7 +62,7 @@ public function testGetButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to delete this?' - ) . '\', \'' . $deleteUrl . '\')', + ) . '\', \'' . $deleteUrl . '\', {data: {}})', 'sort_order' => 20, ]; diff --git a/app/code/Magento/SalesRule/composer.json b/app/code/Magento/SalesRule/composer.json index 752c711ff4c3a..bf19520c7b552 100644 --- a/app/code/Magento/SalesRule/composer.json +++ b/app/code/Magento/SalesRule/composer.json @@ -25,7 +25,7 @@ "magento/module-sales-rule-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.5", + "version": "101.0.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php b/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php index a73edcce99760..6c54d80a61319 100644 --- a/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php +++ b/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php @@ -24,7 +24,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __('Are you sure you want to delete this synonym group?') - . '\', \'' . $this->getDeleteUrl() . '\')', + . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php index 9d8b612cefadf..e531e947a5ab5 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php @@ -55,7 +55,7 @@ public function execute() $id = $this->getRequest()->getParam('group_id'); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); - if ($id) { + if ($this->getRequest()->isPost() && $id) { try { /** @var \Magento\Search\Model\SynonymGroup $synGroupModel */ $synGroupModel = $this->synGroupRepository->get($id); diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php index f2770f77cc533..2f3d574e21485 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php @@ -6,8 +6,10 @@ namespace Magento\Search\Controller\Adminhtml\Synonyms; +use Magento\Framework\Exception\NotFoundException; + /** - * Mass-Delete Controller + * Mass-Delete Controller. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -56,13 +58,17 @@ public function __construct( } /** - * Execute action + * Execute action. * * @return \Magento\Backend\Model\View\Result\Redirect - * @throws \Magento\Framework\Exception\LocalizedException|\Exception + * @throws \Magento\Framework\Exception\NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + $collection = $this->filter->getCollection($this->collectionFactory->create()); $collectionSize = $collection->getSize(); @@ -88,6 +94,7 @@ public function execute() } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT); + return $resultRedirect->setPath('*/*/'); } } diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php b/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php index c7adf32da0fb0..52ff4a0388634 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php @@ -5,6 +5,7 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\Exception\NotFoundException; use Magento\Search\Controller\Adminhtml\Term as TermController; use Magento\Framework\Controller\ResultFactory; @@ -12,10 +13,15 @@ class Delete extends TermController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { - $id = $this->getRequest()->getParam('id'); + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + + $id = (int)$this->getRequest()->getParam('id'); /** @var \Magento\Backend\Model\View\Result\Redirect $redirectResult */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); if ($id) { diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php b/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php index f6874078f2f64..449450eeb8fd7 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php @@ -5,6 +5,7 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\Exception\NotFoundException; use Magento\Search\Controller\Adminhtml\Term as TermController; use Magento\Framework\Controller\ResultFactory; @@ -12,9 +13,14 @@ class MassDelete extends TermController { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $searchIds = $this->getRequest()->getParam('search'); if (!is_array($searchIds)) { $this->messageManager->addErrorMessage(__('Please select searches.')); diff --git a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/DeleteTest.php b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/DeleteTest.php index 38c78b986faf4..164f82da02f6f 100644 --- a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/DeleteTest.php +++ b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/DeleteTest.php @@ -55,8 +55,9 @@ protected function setUp() false, true, true, - ['getParam'] + ['getParam', 'isPost'] ); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManager\ObjectManager::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/MassDeleteTest.php b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/MassDeleteTest.php new file mode 100644 index 0000000000000..8b98959225528 --- /dev/null +++ b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Synonyms/MassDeleteTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\Test\Unit\Controller\Adminhtml\Synonyms; + +/** + * Unit tests for Magento\Search\Controller\Adminhtml\Synonyms\MassDelete controller. + */ +class MassDeleteTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Search\Controller\Adminhtml\Synonyms\MassDelete + */ + private $controller; + + /** + * @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject + */ + private $filterMock; + + /** + * @var \Magento\Search\Model\ResourceModel\SynonymGroup\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $collectionFactoryMock; + + /** + * @var \Magento\Search\Api\SynonymGroupRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $synGroupRepositoryMock; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->requestMock = $this->getMockForAbstractClass( + \Magento\Framework\App\RequestInterface::class, + [], + '', + false, + true, + true, + ['isPost'] + ); + $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); + $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); + $this->collectionFactoryMock = $this->createMock( + \Magento\Search\Model\ResourceModel\SynonymGroup\CollectionFactory::class + ); + $this->synGroupRepositoryMock = $this->createMock(\Magento\Search\Api\SynonymGroupRepositoryInterface::class); + + $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); + + $this->controller = $objectManagerHelper->getObject( + \Magento\Search\Controller\Adminhtml\Synonyms\MassDelete::class, + [ + 'context' => $this->contextMock, + 'filter' => $this->filterMock, + 'collectionFactory' => $this->collectionFactoryMock, + 'synGroupRepository' => $this->synGroupRepositoryMock, + ] + ); + } + + /** + * Check that error throws when request is not POST. + * + * @return void + * @expectedException \Magento\Framework\Exception\NotFoundException + */ + public function testExecuteWithNotPostRequest() + { + $this->requestMock->expects($this->once())->method('isPost')->willReturn(false); + + $this->controller->execute(); + } +} diff --git a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/MassDeleteTest.php b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/MassDeleteTest.php index 60cc958a6187c..a9a705b74a8bb 100644 --- a/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/MassDeleteTest.php +++ b/app/code/Magento/Search/Test/Unit/Controller/Adminhtml/Term/MassDeleteTest.php @@ -46,7 +46,7 @@ protected function setUp() { $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->disableOriginalConstructor() - ->setMethods([]) + ->setMethods(['isPost']) ->getMockForAbstractClass(); $this->objectManager = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) ->disableOriginalConstructor() @@ -85,6 +85,7 @@ protected function setUp() $this->context->expects($this->any()) ->method('getResultFactory') ->willReturn($this->resultFactoryMock); + $this->request->expects($this->any())->method('isPost')->willReturn(true); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->controller = $this->objectManagerHelper->getObject( diff --git a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php index 8cc9b809ff888..5495b93a0828e 100644 --- a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php +++ b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php @@ -63,7 +63,8 @@ public function prepareDataSource(array $dataSource) 'confirm' => [ 'title' => __('Delete'), 'message' => __('Are you sure you want to delete synonym group with id: %1?', $item['group_id']) - ] + ], + 'post' => true, ]; $item[$name]['edit'] = [ 'href' => $this->urlBuilder->getUrl(self::SYNONYM_URL_PATH_EDIT, ['group_id' => $item['group_id']]), diff --git a/app/code/Magento/Search/composer.json b/app/code/Magento/Search/composer.json index 3515ce33a4ee5..0ade5ca67b6ee 100644 --- a/app/code/Magento/Search/composer.json +++ b/app/code/Magento/Search/composer.json @@ -11,7 +11,7 @@ "magento/module-ui": "101.0.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Security/composer.json b/app/code/Magento/Security/composer.json index 4edfa9c55e2ee..c3e632a8216a4 100644 --- a/app/code/Magento/Security/composer.json +++ b/app/code/Magento/Security/composer.json @@ -11,7 +11,7 @@ "magento/module-customer": "101.0.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/SendFriend/Block/Send.php b/app/code/Magento/SendFriend/Block/Send.php index 43e95ebe43d48..1c4b550361359 100644 --- a/app/code/Magento/SendFriend/Block/Send.php +++ b/app/code/Magento/SendFriend/Block/Send.php @@ -5,6 +5,7 @@ */ namespace Magento\SendFriend\Block; +use Magento\Captcha\Block\Captcha; use Magento\Customer\Model\Context; /** @@ -170,6 +171,7 @@ public function setFormData($data) /** * Retrieve Current Product Id * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @return int */ public function getProductId() @@ -180,6 +182,7 @@ public function getProductId() /** * Retrieve current category id for product * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @return int */ public function getCategoryId() @@ -222,4 +225,24 @@ public function canSend() { return !$this->sendfriend->isExceedLimit(); } + + /** + * @inheritdoc + */ + protected function _prepareLayout() + { + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => 'product_sendtofriend_form', + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + } } diff --git a/app/code/Magento/SendFriend/Controller/Product/Sendmail.php b/app/code/Magento/SendFriend/Controller/Product/Sendmail.php index 72dcf00d17c7d..36365ddfb44e5 100644 --- a/app/code/Magento/SendFriend/Controller/Product/Sendmail.php +++ b/app/code/Magento/SendFriend/Controller/Product/Sendmail.php @@ -4,13 +4,18 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\SendFriend\Controller\Product; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\App\ObjectManager; +use Magento\SendFriend\Model\CaptchaValidator; +/** + * Controller class Sendmail. Represents send-mail action request flow + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Sendmail extends \Magento\SendFriend\Controller\Product { /** @@ -23,6 +28,11 @@ class Sendmail extends \Magento\SendFriend\Controller\Product */ protected $catalogSession; + /** + * @var CaptchaValidator + */ + private $captchaValidator; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry @@ -31,6 +41,9 @@ class Sendmail extends \Magento\SendFriend\Controller\Product * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository * @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository * @param \Magento\Catalog\Model\Session $catalogSession + * @param CaptchaValidator|null $captchaValidator + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -39,11 +52,13 @@ public function __construct( \Magento\SendFriend\Model\SendFriend $sendFriend, \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository, - \Magento\Catalog\Model\Session $catalogSession + \Magento\Catalog\Model\Session $catalogSession, + CaptchaValidator $captchaValidator = null ) { parent::__construct($context, $coreRegistry, $formKeyValidator, $sendFriend, $productRepository); $this->categoryRepository = $categoryRepository; $this->catalogSession = $catalogSession; + $this->captchaValidator = $captchaValidator ?: ObjectManager::getInstance()->create(CaptchaValidator::class); } /** @@ -91,6 +106,7 @@ public function execute() try { $validate = $this->sendFriend->validate(); + $this->captchaValidator->validateSending($this->getRequest()); if ($validate === true) { $this->sendFriend->send(); $this->messageManager->addSuccess(__('The link to a friend was sent.')); diff --git a/app/code/Magento/SendFriend/Model/CaptchaValidator.php b/app/code/Magento/SendFriend/Model/CaptchaValidator.php new file mode 100644 index 0000000000000..20082e4880b4c --- /dev/null +++ b/app/code/Magento/SendFriend/Model/CaptchaValidator.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\SendFriend\Model; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\DefaultModel; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; + +/** + * Class CaptchaValidator. Performs captcha validation + */ +class CaptchaValidator +{ + /** + * @var Data + */ + private $captchaHelper; + + /** + * @var CaptchaStringResolver + */ + private $captchaStringResolver; + + /** + * @var UserContextInterface + */ + private $currentUser; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * CaptchaValidator constructor. + * + * @param Data $captchaHelper + * @param CaptchaStringResolver $captchaStringResolver + * @param UserContextInterface $currentUser + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + Data $captchaHelper, + CaptchaStringResolver $captchaStringResolver, + UserContextInterface $currentUser, + CustomerRepositoryInterface $customerRepository + ) { + $this->captchaHelper = $captchaHelper; + $this->captchaStringResolver = $captchaStringResolver; + $this->currentUser = $currentUser; + $this->customerRepository = $customerRepository; + } + + /** + * Entry point for captcha validation + * + * @param RequestInterface $request + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function validateSending(RequestInterface $request) + { + $this->validateCaptcha($request); + } + + /** + * Validates captcha and triggers log attempt + * + * @param RequestInterface $request + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function validateCaptcha(RequestInterface $request) + { + $captchaTargetFormName = 'product_sendtofriend_form'; + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha($captchaTargetFormName); + + if ($captchaModel->isRequired()) { + $word = $this->captchaStringResolver->resolve( + $request, + $captchaTargetFormName + ); + + $isCorrectCaptcha = $captchaModel->isCorrect($word); + + if (!$isCorrectCaptcha) { + $this->logCaptchaAttempt($captchaModel); + throw new LocalizedException(__('Incorrect CAPTCHA')); + } + } + + $this->logCaptchaAttempt($captchaModel); + } + + /** + * Log captcha attempts + * + * @param DefaultModel $captchaModel + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function logCaptchaAttempt(DefaultModel $captchaModel) + { + $email = ''; + + if ($this->currentUser->getUserType() == UserContextInterface::USER_TYPE_CUSTOMER) { + $email = $this->customerRepository->getById($this->currentUser->getUserId())->getEmail(); + } + + $captchaModel->logAttempt($email); + } +} diff --git a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php b/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php deleted file mode 100644 index c7881f366f520..0000000000000 --- a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php +++ /dev/null @@ -1,906 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\SendFriend\Test\Unit\Controller\Product; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SendmailTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\SendFriend\Controller\Product\Sendmail */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registryMock; - - /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $validatorMock; - - /** @var \Magento\SendFriend\Model\SendFriend|\PHPUnit_Framework_MockObject_MockObject */ - protected $sendFriendMock; - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productRepositoryMock; - - /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryRepositoryMock; - - /** @var \Magento\Catalog\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogSessionMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - /** @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultFactoryMock; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; - - /** @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirectMock; - - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlBuilderMock; - - protected function setUp() - { - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->setMethods(['getPost', 'getPostValue', 'getParam']) - ->getMockForAbstractClass(); - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->getMock(); - $this->validatorMock = $this->getMockBuilder(\Magento\Framework\Data\Form\FormKey\Validator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->sendFriendMock = $this->getMockBuilder(\Magento\SendFriend\Model\SendFriend::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->categoryRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\CategoryRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->catalogSessionMock = $this->getMockBuilder(\Magento\Catalog\Model\Session::class) - ->setMethods(['getSendfriendFormData', 'setSendfriendFormData']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMock(); - $this->resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->getMock(); - $this->redirectMock = $this->getMockBuilder(\Magento\Framework\App\Response\RedirectInterface::class) - ->getMock(); - $this->urlBuilderMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) - ->getMock(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\SendFriend\Controller\Product\Sendmail::class, - [ - 'request' => $this->requestMock, - 'coreRegistry' => $this->registryMock, - 'formKeyValidator' => $this->validatorMock, - 'sendFriend' => $this->sendFriendMock, - 'productRepository' => $this->productRepositoryMock, - 'categoryRepository' => $this->categoryRepositoryMock, - 'catalogSession' => $this->catalogSessionMock, - 'messageManager' => $this->messageManagerMock, - 'resultFactory' => $this->resultFactoryMock, - 'eventManager' => $this->eventManagerMock, - 'redirect' => $this->redirectMock, - 'url' => $this->urlBuilderMock, - ] - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $productUrl = 'product_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - /** @var \Magento\Catalog\Api\Data\CategoryInterface|\PHPUnit_Framework_MockObject_MockObject $categoryMock */ - $categoryMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\CategoryInterface::class) - ->getMockForAbstractClass(); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willReturn($categoryMock); - - $productMock->expects($this->once()) - ->method('setCategory') - ->with($categoryMock); - - $this->registryMock->expects($this->exactly(2)) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ['current_category', $categoryMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn(true); - $this->sendFriendMock->expects($this->once()) - ->method('send') - ->willReturnSelf(); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with(__('The link to a friend was sent.')) - ->willReturnSelf(); - - $productMock->expects($this->once()) - ->method('getProductUrl') - ->willReturn($productUrl); - - $this->redirectMock->expects($this->once()) - ->method('success') - ->with($productUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($productUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutValidationAndCategory() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn(['Some error']); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Some error')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutValidationAndCategoryWithProblems() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn('Some error'); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('We found some problems with the data.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithLocalizedException() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('Localized Exception.'))); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Localized Exception.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithException() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $exception = new \Exception(__('Exception.')); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willThrowException($exception); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addException') - ->with($exception, __('Some emails were not sent.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutProduct() - { - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->exactly(2)) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - [\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, [], $forwardMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutData() - { - $productId = 11; - $formData = ''; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->exactly(2)) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - [\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, [], $forwardMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - public function testExecuteWithoutFormKey() - { - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(false); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/SendFriend/composer.json b/app/code/Magento/SendFriend/composer.json index e6af6d9dcfbef..de252db3f3969 100644 --- a/app/code/Magento/SendFriend/composer.json +++ b/app/code/Magento/SendFriend/composer.json @@ -6,10 +6,12 @@ "magento/module-store": "100.2.*", "magento/module-catalog": "102.0.*", "magento/module-customer": "101.0.*", - "magento/framework": "101.0.*" + "magento/framework": "101.0.*", + "magento/module-captcha": "100.2.*", + "magento/module-authorization": "100.2.*" }, "type": "magento2-module", - "version": "100.2.3", + "version": "100.2.4", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/SendFriend/etc/config.xml b/app/code/Magento/SendFriend/etc/config.xml index 9fa005dcd2fd4..d65e5a4a073dd 100644 --- a/app/code/Magento/SendFriend/etc/config.xml +++ b/app/code/Magento/SendFriend/etc/config.xml @@ -17,5 +17,21 @@ <check_by>0</check_by> </email> </sendfriend> + <captcha translate="label"> + <frontend> + <areas> + <product_sendtofriend_form> + <label>Send To Friend Form</label> + </product_sendtofriend_form> + </areas> + </frontend> + </captcha> + <customer> + <captcha> + <shown_to_logged_in_user> + <product_sendtofriend_form>1</product_sendtofriend_form> + </shown_to_logged_in_user> + </captcha> + </customer> </default> </config> diff --git a/app/code/Magento/SendFriend/etc/module.xml b/app/code/Magento/SendFriend/etc/module.xml index fae2b90f710a3..c874c54cbc672 100644 --- a/app/code/Magento/SendFriend/etc/module.xml +++ b/app/code/Magento/SendFriend/etc/module.xml @@ -10,6 +10,7 @@ <module name="Magento_SendFriend" setup_version="2.0.0"> <sequence> <module name="Magento_Catalog"/> + <module name="Magento_Captcha"/> </sequence> </module> </config> diff --git a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml index 8065b7e236132..4d6f3d8c628b2 100644 --- a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml +++ b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml @@ -13,7 +13,7 @@ </action> </referenceBlock> <referenceContainer name="content"> - <block class="Magento\SendFriend\Block\Send" name="sendfriend.send" template="Magento_SendFriend::send.phtml"> + <block class="Magento\SendFriend\Block\Send" name="sendfriend.send" cacheable="false" template="Magento_SendFriend::send.phtml"> <container name="form.additional.info" as="form_additional_info"/> </block> </referenceContainer> diff --git a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml index 4922a9f365ced..3e00353a9157d 100644 --- a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml +++ b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml @@ -108,6 +108,7 @@ </div> <?= $block->getChildHtml('form_additional_info') ?> </fieldset> + <?= $block->getChildHtml('captcha'); ?> <div class="actions-toolbar"> <div class="primary"> <button type="submit" diff --git a/app/code/Magento/Shipping/Block/Adminhtml/View.php b/app/code/Magento/Shipping/Block/Adminhtml/View.php index 04df7f6e35e24..711dd2f46c1c6 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/View.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/View.php @@ -58,7 +58,7 @@ protected function _construct() 'onclick', "deleteConfirm('" . __( 'Are you sure you want to send a Shipment email to customer?' - ) . "', '" . $this->getEmailUrl() . "')" + ) . "', '" . $this->getEmailUrl() . "', {data: {}})" ); } diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php index 5eca94bce562b..5a81bdb3cf32f 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php @@ -6,7 +6,6 @@ */ namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; -use Magento\Backend\App\Action; use Magento\Sales\Model\Order\Shipment\Validation\QuantityValidator; /** @@ -97,7 +96,7 @@ public function execute() $formKeyIsValid = $this->_formKeyValidator->validate($this->getRequest()); $isPost = $this->getRequest()->isPost(); if (!$formKeyIsValid || !$isPost) { - $this->messageManager->addError(__('We can\'t save the shipment right now.')); + $this->messageManager->addErrorMessage(__('We can\'t save the shipment right now.')); return $resultRedirect->setPath('sales/order/index'); } @@ -108,7 +107,6 @@ public function execute() } $isNeedCreateLabel = isset($data['create_shipping_label']) && $data['create_shipping_label']; - $responseAjax = new \Magento\Framework\DataObject(); try { @@ -136,7 +134,7 @@ public function execute() ->validate($shipment, [QuantityValidator::class]); if ($validationResult->hasMessages()) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __("Shipment Document Validation Error(s):\n" . implode("\n", $validationResult->getMessages())) ); $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); @@ -160,7 +158,7 @@ public function execute() $shipmentCreatedMessage = __('The shipment has been created.'); $labelCreatedMessage = __('You created the shipping label.'); - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( $isNeedCreateLabel ? $shipmentCreatedMessage . ' ' . $labelCreatedMessage : $shipmentCreatedMessage ); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getCommentText(true); @@ -169,7 +167,7 @@ public function execute() $responseAjax->setError(true); $responseAjax->setMessage($e->getMessage()); } else { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } } catch (\Exception $e) { @@ -178,7 +176,7 @@ public function execute() $responseAjax->setError(true); $responseAjax->setMessage(__('An error occurred while creating shipping label.')); } else { - $this->messageManager->addError(__('Cannot save shipment.')); + $this->messageManager->addErrorMessage(__('Cannot save shipment.')); $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } } diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php index 491a1e01f1720..730088c76eada 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php @@ -135,7 +135,7 @@ protected function setUp() $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor()->getMock(); $this->objectManager = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, ['create', 'get']); - $this->messageManager = $this->createPartialMock(\Magento\Framework\Message\Manager::class, ['addSuccess', 'addError']); + $this->messageManager = $this->createPartialMock(\Magento\Framework\Message\Manager::class, ['addSuccessMessage', 'addErrorMessage']); $this->session = $this->createPartialMock(\Magento\Backend\Model\Session::class, ['setIsUrlNotice', 'getCommentText']); $this->actionFlag = $this->createPartialMock(\Magento\Framework\App\ActionFlag::class, ['get']); $this->helper = $this->createPartialMock(\Magento\Backend\Helper\Data::class, ['getUrl']); @@ -216,7 +216,7 @@ public function testExecute($formKeyIsValid, $isPost) if (!$formKeyIsValid || !$isPost) { $this->messageManager->expects($this->once()) - ->method('addError'); + ->method('addErrorMessage'); $this->resultRedirect->expects($this->once()) ->method('setPath') diff --git a/app/code/Magento/Shipping/composer.json b/app/code/Magento/Shipping/composer.json index d887d9c984eb7..a8bb256cdd305 100644 --- a/app/code/Magento/Shipping/composer.json +++ b/app/code/Magento/Shipping/composer.json @@ -25,7 +25,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Signifyd/composer.json b/app/code/Magento/Signifyd/composer.json index 7140bf36518a3..62ecc442c3138 100644 --- a/app/code/Magento/Signifyd/composer.json +++ b/app/code/Magento/Signifyd/composer.json @@ -17,7 +17,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "proprietary" ], diff --git a/app/code/Magento/Sitemap/Block/Adminhtml/Edit.php b/app/code/Magento/Sitemap/Block/Adminhtml/Edit.php index d1b5fd1df45c4..ff3334dc38531 100644 --- a/app/code/Magento/Sitemap/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Sitemap/Block/Adminhtml/Edit.php @@ -5,6 +5,9 @@ */ namespace Magento\Sitemap\Block\Adminhtml; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Helper\PostHelper; + /** * Sitemap edit form container * @@ -19,18 +22,26 @@ class Edit extends \Magento\Backend\Block\Widget\Form\Container */ protected $_coreRegistry = null; + /** + * @var PostHelper + */ + private $postHelper; + /** * @param \Magento\Backend\Block\Widget\Context $context * @param \Magento\Framework\Registry $registry * @param array $data + * @param PostHelper|null $postHelper */ public function __construct( \Magento\Backend\Block\Widget\Context $context, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + PostHelper $postHelper = null ) { $this->_coreRegistry = $registry; parent::__construct($context, $data); + $this->postHelper = $postHelper ?: ObjectManager::getInstance()->create(PostHelper::class); } /** @@ -64,6 +75,30 @@ protected function _construct() ); } + /** + * @inheritdoc + */ + protected function _prepareLayout() + { + $this->buttonList->update( + 'delete', + '', + [ + 'label' => __('Delete'), + 'class' => 'delete', + 'onclick' => 'deleteConfirm(\'Are you sure you want to do this?\', \'' . + $this->getDeleteUrl() . '\','.$this->postHelper->getPostData($this->getDeleteUrl()).')', + 'id' => 'delete', + 'button_key' => 'delete_button', + 'region' => 'toolbar', + 'level' => 0, + 'sort_order' => 10 + ] + ); + + parent::_prepareLayout(); + } + /** * Get edit form container header text * diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php index 12d89d899fa67..fe2b9d5a846a6 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NotFoundException; class Delete extends \Magento\Sitemap\Controller\Adminhtml\Sitemap { @@ -39,9 +40,15 @@ public function __construct( * Delete action * * @return void + * @throws NotFoundException + * @throws \Magento\Framework\Exception\FileSystemException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + $directory = $this->getFilesystem()->getDirectoryWrite(DirectoryList::ROOT); // check if we know what should be deleted $id = $this->getRequest()->getParam('sitemap_id'); diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php index 1e0d1cb248f00..55a04d52aac93 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php @@ -8,6 +8,7 @@ use Magento\Backend\App\Action; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Controller; +use Magento\Framework\Exception\NotFoundException; class Save extends \Magento\Sitemap\Controller\Adminhtml\Sitemap { @@ -132,9 +133,14 @@ protected function getResult($id) * Save action * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + // check if data sent $data = $this->getRequest()->getPostValue(); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index dda1697da7fdf..7279f6eda85d7 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -1,4 +1,5 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -155,12 +156,11 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento protected $dateTime; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [Value::CACHE_TAG]; /** * Last mode min timestamp value diff --git a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php index b56ed39ba16fc..6ac9ba2d91af9 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php @@ -49,7 +49,7 @@ protected function setUp() ->getMock(); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->disableOriginalConstructor() - ->setMethods([]) + ->setMethods(['isPost']) ->getMockForAbstractClass(); $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\ResponseInterface::class) ->disableOriginalConstructor() @@ -81,6 +81,7 @@ public function testExecuteWithoutSitemapId() ->getMockForAbstractClass(); $this->filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($writeDirectoryMock); $this->requestMock->expects($this->any())->method('getParam')->with('sitemap_id')->willReturn(0); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $this->responseMock->expects($this->once())->method('setRedirect'); $this->messageManagerMock->expects($this->any()) ->method('addError') @@ -97,6 +98,7 @@ public function testExecuteCannotFindSitemap() ->getMockForAbstractClass(); $this->filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($writeDirectoryMock); $this->requestMock->expects($this->any())->method('getParam')->with('sitemap_id')->willReturn($id); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $sitemapMock = $this->getMockBuilder(\Magento\Sitemap\Model\Sitemap::class) ->disableOriginalConstructor() @@ -123,6 +125,7 @@ public function testExecute() ->getMockForAbstractClass(); $this->filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($writeDirectoryMock); $this->requestMock->expects($this->any())->method('getParam')->with('sitemap_id')->willReturn($id); + $this->requestMock->expects($this->any())->method('isPost')->willReturn(true); $sitemapMock = $this->getMockBuilder(\Magento\Sitemap\Model\Sitemap::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php index f77954101df7c..e540f5a9c382f 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/SaveTest.php @@ -54,7 +54,7 @@ protected function setUp() { $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->disableOriginalConstructor() - ->setMethods(['getPostValue']) + ->setMethods(['getPostValue', 'isPost']) ->getMockForAbstractClass(); $this->resultRedirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) ->disableOriginalConstructor() @@ -95,6 +95,8 @@ public function testSaveEmptyDataShouldRedirectToDefault() $this->requestMock->expects($this->once()) ->method('getPostValue') ->willReturn([]); + $this->requestMock->expects($this->any()) + ->method('isPost')->willReturn(true); $this->resultRedirectMock->expects($this->once()) ->method('setPath') ->with('adminhtml/*/') @@ -116,6 +118,8 @@ public function testTryToSaveInvalidDataShouldFailWithErrors() $this->requestMock->expects($this->once()) ->method('getPostValue') ->willReturn($data); + $this->requestMock->expects($this->any()) + ->method('isPost')->willReturn(true); $this->requestMock->expects($this->once()) ->method('getParam') ->with('sitemap_id') diff --git a/app/code/Magento/Sitemap/composer.json b/app/code/Magento/Sitemap/composer.json index 40f2c153f33be..acc5c9d8f33a0 100644 --- a/app/code/Magento/Sitemap/composer.json +++ b/app/code/Magento/Sitemap/composer.json @@ -18,7 +18,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Store/Model/ResourceModel/Website/Grid/Collection.php b/app/code/Magento/Store/Model/ResourceModel/Website/Grid/Collection.php index e42b226653b44..259d41381170d 100644 --- a/app/code/Magento/Store/Model/ResourceModel/Website/Grid/Collection.php +++ b/app/code/Magento/Store/Model/ResourceModel/Website/Grid/Collection.php @@ -10,6 +10,18 @@ */ class Collection extends \Magento\Store\Model\ResourceModel\Website\Collection { + /** + * @inheritdoc + */ + protected function _construct() + { + parent::_construct(); + + $this->_map['fields']['store_title'] = 'store_table.name'; + $this->_map['fields']['group_title'] = 'group_table.name'; + $this->_map['fields']['name'] = 'main_table.name'; + } + /** * Join website and store names * @@ -21,4 +33,37 @@ protected function _initSelect() $this->joinGroupAndStore(); return $this; } + + /** + * @inheritdoc + */ + public function load($printQuery = false, $logQuery = false) + { + if ($this->isLoaded()) { + return $this; + } + + return $this->loadWithFilter($printQuery, $logQuery); + } + + /** + * @inheritdoc + */ + public function joinGroupAndStore() + { + if (!$this->getFlag('groups_and_stores_joined')) { + $this->_idFieldName = 'website_group_store'; + $this->getSelect()->joinLeft( + ['group_table' => $this->getTable('store_group')], + 'main_table.website_id = group_table.website_id', + ['group_id' => 'group_id', 'group_title' => 'name', 'group_code' => 'code'] + )->joinLeft( + ['store_table' => $this->getTable('store')], + 'group_table.group_id = store_table.group_id', + ['store_id' => 'store_id', 'store_title' => 'name', 'store_code' => 'code'] + ); + } + + return $this; + } } diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml index f241531f61ffd..0b9676b14b776 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml @@ -22,7 +22,7 @@ <fillField selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" userInput="{{storeGroupCode}}" stepKey="enterStoreGroupCode" /> <selectOption selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="Default Category" stepKey="chooseRootCategory" /> <click selector="{{AdminStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreGroup" /> - <waitForElementVisible selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="waitForStoreGridReload"/> + <waitForElementVisible selector="{{AdminStoresGridFilterSection.filters}}" stepKey="waitForStoreGridReload"/> <see userInput="You saved the store." stepKey="seeSavedMessage" /> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml index 9d7c538d3a3c4..10303684e19d5 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml @@ -20,7 +20,7 @@ <fillField selector="{{AdminNewWebsiteSection.name}}" userInput="{{newWebsiteName}}" stepKey="enterWebsiteName" /> <fillField selector="{{AdminNewWebsiteSection.code}}" userInput="{{websiteCode}}" stepKey="enterWebsiteCode" /> <click selector="{{AdminNewWebsiteActionsSection.saveWebsite}}" stepKey="clickSaveWebsite" /> - <waitForElementVisible selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="waitForStoreGridToReload"/> + <waitForElementVisible selector="{{AdminStoresGridFilterSection.filters}}" stepKey="waitForStoreGridToReload"/> <see userInput="You saved the website." stepKey="seeSavedMessage" /> </actionGroup> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml index 8b059ac164c0f..77473ae235c4a 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml @@ -14,9 +14,12 @@ </arguments> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> <waitForPageLoad stepKey="waitStoreIndexPageLoad" /> - <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilterField"/> - <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearch"/> - <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewInGrid"/> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.storeViewFilter}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilterField"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <click selector="{{AdminStoresGridSection.storeViewInFirstRow}}" stepKey="clickStoreViewInGrid"/> <waitForPageLoad stepKey="waitForStoreViewPage"/> <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreView"/> <selectOption selector="{{AdminStoreBackupOptionsSection.createBackupSelect}}" userInput="No" stepKey="dontCreateDbBackup"/> @@ -29,6 +32,6 @@ <arguments> <argument name="customStoreName" type="string"/> </arguments> - <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{customStoreName}}" stepKey="fillStoreViewFilterField"/> + <fillField selector="{{AdminStoresGridFilterSection.storeViewFilter}}" userInput="{{customStoreName}}" stepKey="fillStoreViewFilterField"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml index 395ba02d5a9de..e809f09565fd6 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml @@ -12,16 +12,18 @@ <argument name="websiteName" type="string"/> </arguments> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> - <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> - <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> - <see userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="verifyThatCorrectWebsiteFound"/> - <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.websiteFilter}}" userInput="{{websiteName}}" stepKey="fillWebsiteFilter"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <see userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteInFirstRow}}" stepKey="verifyThatCorrectWebsiteFound"/> + <click selector="{{AdminStoresGridSection.websiteInFirstRow}}" stepKey="clickEditExistingStoreRow"/> <waitForPageLoad stepKey="waitForStoreToLoad"/> <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteWebsiteButtonOnEditWebsitePage"/> <selectOption userInput="No" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteWebsiteButton"/> - <waitForElementVisible selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="waitForStoreGridToReload"/> + <waitForElementVisible selector="{{AdminStoresGridFilterSection.filters}}" stepKey="waitForStoreGridToReload"/> <see userInput="You deleted the website." stepKey="seeSavedMessage"/> <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> </actionGroup> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoresFilterGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoresFilterGridActionGroup.xml new file mode 100644 index 0000000000000..2ccfc0dcc1277 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoresFilterGridActionGroup.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="filterStoresGridByWebsite"> + <arguments> + <argument name="website" type="string"/> + </arguments> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.websiteFilter}}" userInput="{{website}}" stepKey="fillWebsiteFilter"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + </actionGroup> + + <actionGroup name="filterStoresGridByStore"> + <arguments> + <argument name="store" type="string"/> + </arguments> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.storeFilter}}" userInput="{{store}}" stepKey="fillStoreFilter"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + </actionGroup> + + <actionGroup name="filterStoresGridByStoreView"> + <arguments> + <argument name="storeView" type="string"/> + </arguments> + <conditionalClick selector="{{AdminStoresGridFilterSection.clearFilters}}" dependentSelector="{{AdminStoresGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField selector="{{AdminStoresGridFilterSection.storeViewFilter}}" userInput="{{storeView}}" stepKey="fillStoreViewFilter"/> + <click selector="{{AdminStoresGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml index 1c56c3cc44220..3ca45a5a378fd 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml @@ -29,4 +29,9 @@ <waitForElementVisible selector="{{AdminMainActionsSection.websiteByName(scopeName)}}" after="clickScopeSwitchDropdown" stepKey="waitForWebsiteNameIsVisible"/> <click selector="{{AdminMainActionsSection.websiteByName(scopeName)}}" after="waitForWebsiteNameIsVisible" stepKey="clickStoreViewByName"/> </actionGroup> + + <actionGroup name="AdminSwitchToAllStoreViewActionGroup" extends="AdminSwitchBaseActionGroup"> + <click selector="{{AdminMainActionsSection.allStoreViews}}" after="clickScopeSwitchDropdown" stepKey="clickStoreViewByName"/> + <see selector="{{AdminMainActionsSection.storeSwitcher}}" userInput="{{scopeName}}" after="clickStoreViewByName" stepKey="seeNewStoreViewName"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml index 7c18ca5375c93..d35cf13890114 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml @@ -13,11 +13,12 @@ <argument name="storeGroupName" defaultValue="customStoreGroup.name"/> </arguments> <amOnPage stepKey="amOnAdminSystemStorePage" url="{{AdminSystemStorePage.url}}"/> - <click stepKey="resetSearchFilter" selector="{{AdminStoresGridSection.resetButton}}"/> - <fillField stepKey="fillSearchStoreGroupField" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="{{storeGroupName}}"/> - <click stepKey="clickSearchButton" selector="{{AdminStoresGridSection.searchButton}}"/> - <see stepKey="verifyThatCorrectStoreGroupFound" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" userInput="{{storeGroupName}}"/> - <click stepKey="clickEditExistingStoreRow" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}"/> + <conditionalClick stepKey="resetSearchFilter" selector="{{AdminStoresGridFilterSection.clearAll}}" dependentSelector="{{AdminStoresGridFilterSection.clearAll}}" visible="true"/> + <click selector="{{AdminStoresGridFilterSection.filters}}" stepKey="openStoresFilters"/> + <fillField stepKey="fillSearchStoreGroupField" selector="{{AdminStoresGridFilterSection.storeFilter}}" userInput="{{storeGroupName}}"/> + <click stepKey="clickSearchButton" selector="{{AdminStoresGridFilterSection.applyFilters}}"/> + <see stepKey="verifyThatCorrectStoreGroupFound" selector="{{AdminStoresGridSection.storeInFirstRow}}" userInput="{{storeGroupName}}"/> + <click stepKey="clickEditExistingStoreRow" selector="{{AdminStoresGridSection.storeInFirstRow}}"/> <waitForPageLoad stepKey="waitForStoreToLoad"/> <click stepKey="clickDeleteStoreGroupButtonOnEditStorePage" selector="{{AdminStoresMainActionsSection.deleteButton}}"/> <selectOption stepKey="setCreateDbBackupToNo" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" userInput="No"/> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml index 5877ed383ae16..61146f186923e 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml @@ -102,4 +102,9 @@ <data key="store_type">group</data> <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> </entity> + <entity name="AllStoreViews" type="store"> + <data key="name">All Store Views</data> + <data key="code">allstoreviews</data> + <data key="is_active">1</data> + </entity> </entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml new file mode 100644 index 0000000000000..413540a8f4c12 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="FreeShippingMethodWithMinimumOrderAmount90" type="free_shipping_config_state"> + <requiredEntity type="active">Active</requiredEntity> + <requiredEntity type="free_shipping_subtotal">Price</requiredEntity> + </entity> + <entity name="Active" type="active"> + <data key="value">1</data> + </entity> + <entity name="Price" type="free_shipping_subtotal"> + <data key="value">90</data> + </entity> + + <entity name="ResetFreeShippingMethodWithMinimumOrderAmount90" type="free_shipping_config_state"> + <requiredEntity type="free_shipping_subtotal">DefaultPrice</requiredEntity> + <requiredEntity type="active">DefaultActive</requiredEntity> + </entity> + <entity name="DefaultPrice" type="free_shipping_subtotal"> + <data key="value">0</data> + </entity> + <entity name="DefaultActive" type="active"> + <requiredEntity type="active_inherit">DefaultFreeShipping</requiredEntity> + </entity> + <entity name="DefaultFreeShipping" type="active_inherit"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml new file mode 100644 index 0000000000000..32aa22fecf9cd --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="ChangeFreeShippingConfiguration" dataType="free_shipping_config_state" type="create" auth="adminFormKey" + url="/admin/system_config/save/section/carriers/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="free_shipping_config_state"> + <object key="freeshipping" dataType="free_shipping_config_state"> + <object key="fields" dataType="free_shipping_config_state"> + <object key="active" dataType="active"> + <field key="value">string</field> + <object key="inherit" dataType="active_inherit"> + <field key="value">integer</field> + </object> + </object> + <object key="free_shipping_subtotal" dataType="free_shipping_subtotal"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml index 1a95d88d454e4..7088ba0e1d103 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml @@ -11,7 +11,8 @@ <section name="AdminMainActionsSection"> <element name="storeSwitcher" type="text" selector=".store-switcher"/> <element name="storeViewDropdown" type="button" selector="#store-change-button"/> - <element name="storeViewByName" type="button" selector="//*[@class='store-switcher-store-view ']/a[contains(text(), '{{storeViewName}}')]" timeout="30" parameterized="true"/> + <element name="storeViewByName" type="button" selector="//*[@class='store-switcher-store-view ']/*[contains(text(), '{{storeViewName}}')]" timeout="30" parameterized="true"/> <element name="websiteByName" type="button" selector="//*[@class='store-switcher-website ']/a[contains(text(), '{{websiteName}}')]" timeout="30" parameterized="true"/> + <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridFilterSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridFilterSection.xml new file mode 100644 index 0000000000000..45028b3af1ade --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridFilterSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminStoresGridFilterSection"> + <element name="filters" type="button" selector="button[data-action='grid-filter-expand']"/> + <element name="clearAll" type="button" selector=".admin__data-grid-header .admin__data-grid-filters-current._show .action-clear" timeout="30"/> + <element name="columnsDropdown" type="button" selector=".admin__data-grid-action-columns button.admin__action-dropdown"/> + <element name="viewColumnOption" type="checkbox" selector="//div[contains(@class, '_active')]//div[contains(@class, 'admin__data-grid-action-columns-menu')]//div[@class='admin__field-option']//label[text()='{{col}}']/preceding-sibling::input" parameterized="true"/> + <element name="clearFilters" type="button" selector=".admin__data-grid-header button[data-action='grid-filter-reset']" timeout="30"/> + <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> + <element name="websiteFilter" type="input" selector="input.admin__control-text[name='name']"/> + <element name="storeFilter" type="input" selector="input.admin__control-text[name='group_title']"/> + <element name="storeViewFilter" type="input" selector="input.admin__control-text[name='store_title']"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml index d0a67b18f6cd5..5e2128a4231a4 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml @@ -7,13 +7,8 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> <section name="AdminStoresGridSection"> - <element name="storeGrpFilterTextField" type="input" selector="#storeGrid_filter_group_title"/> - <element name="websiteFilterTextField" type="input" selector="#storeGrid_filter_website_title"/> - <element name="storeFilterTextField" type="input" selector="#storeGrid_filter_store_title"/> - <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]"/> - <element name="resetButton" type="button" selector="button[title='Reset Filter']"/> - <element name="websiteNameInFirstRow" type="text" selector=".col-website_title>a"/> - <element name="storeGrpNameInFirstRow" type="text" selector=".col-group_title>a"/> - <element name="storeNameInFirstRow" type="text" selector=".col-store_title>a"/> + <element name="websiteInFirstRow" type="text" selector=".data-row[data-repeat-index='0'] td:nth-of-type(1) a"/> + <element name="storeInFirstRow" type="text" selector=".data-row[data-repeat-index='0'] td:nth-of-type(2) a"/> + <element name="storeViewInFirstRow" type="text" selector=".data-row[data-repeat-index='0'] td:nth-of-type(3) a"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml index 860fb4a6a9327..bb1dbb3559d74 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupTest.xml @@ -38,19 +38,14 @@ <amOnPage stepKey="openAdminSystemStorePage" url="{{AdminSystemStorePage.url}}"/> - <click stepKey="clickResetButton" selector="{{AdminStoresGridSection.resetButton}}"/> - <waitForPageLoad stepKey="waitForPageLoadAfterReset" time="10"/> + <actionGroup ref="filterStoresGridByStore" stepKey="enterStoreGroup1Name"> + <argument name="store" value="$$createCustomStoreGroup1.group[name]$$"/> + </actionGroup> + <see stepKey="seeStoreGroup1NameAfterSearch" selector="{{AdminStoresGridSection.storeInFirstRow}}" userInput="$$createCustomStoreGroup1.group[name]$$"/> - <fillField stepKey="enterStoreGroup1Name" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="$$createCustomStoreGroup1.group[name]$$"/> - <click stepKey="searchStoreGroup1Name" selector="{{AdminStoresGridSection.searchButton}}"/> - <waitForPageLoad stepKey="waitForPageLoadAfterSearch" time="10"/> - <see stepKey="seeStoreGroup1NameAfterSearch" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" userInput="$$createCustomStoreGroup1.group[name]$$"/> - - <click stepKey="clickResetButton2" selector="{{AdminStoresGridSection.resetButton}}"/> - <waitForPageLoad stepKey="waitForPageLoadAfterReset2" time="10"/> - <fillField stepKey="enterStoreGroup2Name2" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="$$createCustomStoreGroup2.group[name]$$"/> - <click stepKey="searchStoreGroup2Name" selector="{{AdminStoresGridSection.searchButton}}"/> - <waitForPageLoad stepKey="waitForPageLoadAfterSearch2" time="10"/> - <see stepKey="seeStoreGroup1NameAfterSearch2" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" userInput="$$createCustomStoreGroup2.group[name]$$"/> + <actionGroup ref="filterStoresGridByStore" stepKey="enterStoreGroup2Name"> + <argument name="store" value="$$createCustomStoreGroup2.group[name]$$"/> + </actionGroup> + <see stepKey="seeStoreGroup1NameAfterSearch2" selector="{{AdminStoresGridSection.storeInFirstRow}}" userInput="$$createCustomStoreGroup2.group[name]$$"/> </test> </tests> diff --git a/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/GroupTitleTest.php b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/GroupTitleTest.php new file mode 100644 index 0000000000000..7c01a33a1db32 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/GroupTitleTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Ui\Component\Listing\Column; + +use Magento\Store\Ui\Component\Listing\Column\GroupTitle; + +/** + * GroupTitleTest contains unit test for \Magento\Store\Ui\Component\Listing\Column\GroupTitle + */ +class GroupTitleTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var GroupTitle + */ + private $component; + + /** + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + public function setUp() + { + $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) + ->getMockForAbstractClass(); + $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->never())->method('getProcessor')->willReturn($processor); + $this->uiComponentFactory = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); + $this->component = new GroupTitle( + $this->context, + $this->uiComponentFactory + ); + + $this->component->setData('name', 'name'); + } + + /** + * @covers \Magento\Store\Ui\Component\Listing\Column\GroupTitle::prepareDataSource + */ + public function testPrepareDataSource() + { + $dataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => 'Main Website Store', + 'group_id' => 1, + 'group_code' => 'main_website_store', + ] + ] + ] + ]; + + $title = sprintf( + '<a title="Edit Store" href="%s">Main Website Store</a><br />(Code: main_website_store)', + 'http://magento-2-1.dev/admin/system_store/editGroup' + ); + + $expectedDataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => $title, + 'group_id' => 1, + 'group_code' => 'main_website_store', + ] + ] + ] + ]; + + $this->context->expects($this->once()) + ->method('getUrl') + ->with( + 'adminhtml/system_store/editGroup', + ['group_id' => 1] + ) + ->willReturn('http://magento-2-1.dev/admin/system_store/editGroup'); + + $dataSource = $this->component->prepareDataSource($dataSource); + $this->assertEquals($expectedDataSource, $dataSource); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/StoreTitleTest.php b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/StoreTitleTest.php new file mode 100644 index 0000000000000..d1b50acf3b80f --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/StoreTitleTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Ui\Component\Listing\Column; + +use Magento\Store\Ui\Component\Listing\Column\StoreTitle; + +/** + * Class StoreTitleTest contains unit test for \Magento\Store\Ui\Component\Listing\Column\StoreTitle + */ +class StoreTitleTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var StoreTitle + */ + private $component; + + /** + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + public function setUp() + { + $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) + ->getMockForAbstractClass(); + $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->never())->method('getProcessor')->willReturn($processor); + $this->uiComponentFactory = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); + $this->component = new StoreTitle( + $this->context, + $this->uiComponentFactory + ); + + $this->component->setData('name', 'name'); + } + + /** + * @covers \Magento\Store\Ui\Component\Listing\Column\StoreTitle::prepareDataSource + */ + public function testPrepareDataSource() + { + $dataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => 'Default Store View', + 'store_id' => 1, + 'store_code' => 'default', + ] + ] + ] + ]; + + $title = sprintf( + '<a title="Edit Store View" href="%s">Default Store View</a><br />(Code: default)', + 'http://magento-2-1.dev/admin/system_store/editStore' + ); + + $expectedDataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => $title, + 'store_id' => 1, + 'store_code' => 'default', + ] + ] + ] + ]; + + $this->context->expects($this->once()) + ->method('getUrl') + ->with( + 'adminhtml/system_store/editStore', + ['store_id' => 1] + ) + ->willReturn('http://magento-2-1.dev/admin/system_store/editStore'); + + $dataSource = $this->component->prepareDataSource($dataSource); + $this->assertEquals($expectedDataSource, $dataSource); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/WebsiteNameTest.php b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/WebsiteNameTest.php new file mode 100644 index 0000000000000..da9f57e09f28d --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Ui/Component/Listing/Column/WebsiteNameTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Test\Unit\Ui\Component\Listing\Column; + +use Magento\Store\Ui\Component\Listing\Column\WebsiteName; + +/** + * Class WebsiteNameTest contains unit tests for \Magento\Store\Ui\Component\Listing\Column\WebsiteName + */ +class WebsiteNameTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var WebsiteName + */ + private $component; + + /** + * @var \Magento\Framework\View\Element\UiComponent\ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var \Magento\Framework\View\Element\UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + public function setUp() + { + $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) + ->getMockForAbstractClass(); + $processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->never())->method('getProcessor')->willReturn($processor); + $this->uiComponentFactory = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); + $this->component = new WebsiteName( + $this->context, + $this->uiComponentFactory + ); + + $this->component->setData('name', 'name'); + } + + /** + * @covers \Magento\Store\Ui\Component\Listing\Column\WebsiteName::prepareDataSource + */ + public function testPrepareDataSource() + { + $dataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => 'Main Website', + 'website_id' => 1, + 'code' => 'base', + ] + ] + ] + ]; + + $title = sprintf( + '<a title="Edit Store" href="%s">Main Website</a><br />(Code: base)', + 'http://magento-2-1.dev/admin/system_store/editWebsite' + ); + + $expectedDataSource = [ + 'data' => [ + 'items' => [ + [ + 'name' => $title, + 'website_id' => 1, + 'code' => 'base', + ] + ] + ] + ]; + + $this->context->expects($this->once()) + ->method('getUrl') + ->with( + 'adminhtml/system_store/editWebsite', + ['website_id' => 1] + ) + ->willReturn('http://magento-2-1.dev/admin/system_store/editWebsite'); + + $dataSource = $this->component->prepareDataSource($dataSource); + $this->assertEquals($expectedDataSource, $dataSource); + } +} diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/AbstractNameColumn.php b/app/code/Magento/Store/Ui/Component/Listing/Column/AbstractNameColumn.php new file mode 100644 index 0000000000000..1cf0534ea9c6e --- /dev/null +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/AbstractNameColumn.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\Component\Listing\Column; + +/** + * Class AbstractNameColumn + */ +abstract class AbstractNameColumn extends \Magento\Ui\Component\Listing\Columns\Column +{ + /** + * @inheritdoc + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName] = $this->prepareTitle($item); + } + } + } + + return $dataSource; + } + + abstract public function prepareTitle(array $item); +} diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/GroupTitle.php b/app/code/Magento/Store/Ui/Component/Listing/Column/GroupTitle.php new file mode 100644 index 0000000000000..c4af27d888fc2 --- /dev/null +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/GroupTitle.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\Component\Listing\Column; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; + +/** + * Class GroupTitle + */ +class GroupTitle extends AbstractNameColumn +{ + /** + * @inheritdoc + */ + public function prepareTitle(array $item) + { + $fieldName = $this->getData('name'); + $url = $this->context->getUrl( + 'adminhtml/system_store/editGroup', + ['group_id' => $item['group_id']] + ); + + $html = '<a title="' . __('Edit Store') . '" href="' . $url . '">' . + $item[$fieldName]. '</a><br />' . '(' . __('Code') . ': ' . $item['group_code'] . ')'; + + return $html; + } +} diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/StoreTitle.php b/app/code/Magento/Store/Ui/Component/Listing/Column/StoreTitle.php new file mode 100644 index 0000000000000..1133b98032bf3 --- /dev/null +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/StoreTitle.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\Component\Listing\Column; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; + +/** + * Class StoreView + */ +class StoreTitle extends AbstractNameColumn +{ + /** + * @inheritdoc + */ + public function prepareTitle(array $item) + { + $fieldName = $this->getData('name'); + $url = $this->context->getUrl( + 'adminhtml/system_store/editStore', + ['store_id' => $item['store_id']] + ); + + $html = '<a title="' . __('Edit Store View') . '" href="' . $url . '">' . + $item[$fieldName]. '</a><br />' . '(' . __('Code') . ': ' . $item['store_code'] . ')'; + + return $html; + } +} diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/WebsiteName.php b/app/code/Magento/Store/Ui/Component/Listing/Column/WebsiteName.php new file mode 100644 index 0000000000000..e95c556541943 --- /dev/null +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/WebsiteName.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\Component\Listing\Column; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; + +/** + * Class WebsiteName + */ +class WebsiteName extends AbstractNameColumn +{ + /** + * @inheritdoc + */ + public function prepareTitle(array $item) + { + $fieldName = $this->getData('name'); + $url = $this->context->getUrl( + 'adminhtml/system_store/editWebsite', + ['website_id' => $item['website_id']] + ); + + $html = '<a title="' . __('Edit Store') . '" href="' . $url . '">' . + $item[$fieldName]. '</a><br />' . '(' . __('Code') . ': ' . $item['code'] . ')'; + + return $html; + } +} diff --git a/app/code/Magento/Store/Ui/DataProvider/WebsiteDataProvider.php b/app/code/Magento/Store/Ui/DataProvider/WebsiteDataProvider.php new file mode 100644 index 0000000000000..bae17a7eeb632 --- /dev/null +++ b/app/code/Magento/Store/Ui/DataProvider/WebsiteDataProvider.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Store\Ui\DataProvider; + +use Magento\Store\Model\ResourceModel\Website\Grid\CollectionFactory as WebsiteCollectionFactory; + +/** + * Class WebsiteDataProvider + */ +class WebsiteDataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider +{ + /** + * WebsiteDataProvider constructor. + * + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param WebsiteCollectionFactory $collectionFactory + * @param array $meta + * @param array $data + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + WebsiteCollectionFactory $collectionFactory, + array $meta = [], + array $data = [] + ) { + parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); + + $this->collection = $collectionFactory->create(); + } +} diff --git a/app/code/Magento/Store/composer.json b/app/code/Magento/Store/composer.json index 8465d5118a45e..6059432d66038 100644 --- a/app/code/Magento/Store/composer.json +++ b/app/code/Magento/Store/composer.json @@ -14,7 +14,7 @@ "magento/module-deploy": "100.2.*" }, "type": "magento2-module", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Store/view/adminhtml/ui_component/store_listing.xml b/app/code/Magento/Store/view/adminhtml/ui_component/store_listing.xml new file mode 100644 index 0000000000000..4e797b5edf7b6 --- /dev/null +++ b/app/code/Magento/Store/view/adminhtml/ui_component/store_listing.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">store_listing.store_listing_data_source</item> + </item> + </argument> + <settings> + <buttons> + <button name="add"> + <url path="adminhtml/*/newWebsite"/> + <class>primary</class> + <label translate="true">Create Website</label> + </button> + <button name="add_store"> + <url path="adminhtml/*/newStore"/> + <class>add add-store-view</class> + <label translate="true">Create Store View</label> + </button> + <button name="add_group"> + <url path="adminhtml/*/newGroup"/> + <class>add add-store</class> + <label translate="true">Create Store</label> + </button> + </buttons> + <spinner>store_columns</spinner> + <deps> + <dep>store_listing.store_listing_data_source</dep> + </deps> + </settings> + <dataSource name="store_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">store_id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Backend::store</aclResource> + <dataProvider class="Magento\Store\Ui\DataProvider\WebsiteDataProvider" name="store_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>website_id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <listingToolbar name="listing_top"> + <settings> + <sticky>true</sticky> + </settings> + <columnsControls name="columns_controls"/> + <filters name="listing_filters"/> + <paging name="listing_paging"/> + </listingToolbar> + <columns name="store_columns"> + <settings> + </settings> + <column name="name" sortOrder="10" class="Magento\Store\Ui\Component\Listing\Column\WebsiteName"> + <settings> + <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <label translate="true">Web Site</label> + </settings> + </column> + <column name="group_title" sortOrder="20" class="Magento\Store\Ui\Component\Listing\Column\GroupTitle"> + <settings> + <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <label translate="true">Store</label> + </settings> + </column> + <column name="store_title" sortOrder="30" class="Magento\Store\Ui\Component\Listing\Column\StoreTitle"> + <settings> + <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> + <label translate="true">Store View</label> + </settings> + </column> + </columns> +</listing> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml index 8f13860f75ad1..6f51bcf33bd7e 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontSwatchProductWithFileCustomOptionTest"> <annotations> <features value="ConfigurableProduct"/> @@ -50,7 +50,9 @@ <!--Add swatch attribute to configurable product--> <actionGroup ref="CreateConfigurationsWithVisualSwatch" stepKey="createConfigurationsWithVisualSwatch"/> <!--Add custom option to configurable product--> - <actionGroup ref="AddProductCustomOptionFile" stepKey="addCustomOptionToProduct"/> + <actionGroup ref="AddProductCustomOptionFile" stepKey="addCustomOptionToProduct"> + <argument name="option" value="ProductOptionFile"/> + </actionGroup> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> <!--Go to storefront--> diff --git a/app/code/Magento/Swatches/composer.json b/app/code/Magento/Swatches/composer.json index f46f6720d30d8..609fe43345d19 100644 --- a/app/code/Magento/Swatches/composer.json +++ b/app/code/Magento/Swatches/composer.json @@ -20,7 +20,7 @@ "magento/module-swatches-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "proprietary" ], diff --git a/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css index 02a3f0324d955..67a5537e3274f 100644 --- a/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css +++ b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css @@ -150,7 +150,19 @@ } .col-swatch-min-width { - min-width: 30px; + min-width: 65px; +} + +[class^=swatch-col], +[class^=col-]:not(.col-draggable):not(.col-default) { + min-width: 150px; +} + +#swatch-visual-options-panel, +#swatch-text-options-panel, +#manage-options-panel { + overflow: auto; + width: 100%; } .swatches-visual-col.unavailable:after { diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index 3e28982ad44d3..9abbd2b9cbf3e 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -396,7 +396,10 @@ define([ select = $widget._RenderSwatchSelect(item, chooseText), input = $widget._RenderFormInput(item), listLabel = '', - label = ''; + firstSpan = '', + div = '', + subDiv = '', + secondSpan = ''; // Show only swatch controls if ($widget.options.onlySwatches && !$widget.options.jsonSwatchConfig.hasOwnProperty(item.id)) { @@ -404,37 +407,48 @@ define([ } if ($widget.options.enableControlLabel) { - label += - '<span id="' + controlLabelId + '" class="' + classes.attributeLabelClass + '">' + - item.label + - '</span>' + - '<span class="' + classes.attributeSelectedOptionLabelClass + '"></span>'; + firstSpan = document.createElement('span'); + secondSpan = document.createElement('span'); + firstSpan.setAttribute('id', controlLabelId); + firstSpan.setAttribute('class', classes.attributeLabelClass); + firstSpan.textContent = item.label; + secondSpan.setAttribute('class', classes.attributeSelectedOptionLabelClass); } if ($widget.inProductList) { $widget.productForm.append(input); input = ''; - listLabel = 'aria-label="' + item.label + '"'; + listLabel = document.createAttribute('aria-label'); + listLabel.value = item.label; } else { - listLabel = 'aria-labelledby="' + controlLabelId + '"'; + listLabel = document.createAttribute('aria-labelledby'); + listLabel.value = controlLabelId; } + div = document.createElement('div'); + subDiv = document.createElement('div'); + div.setAttribute('class', classes.attributeClass + ' ' + item.code); + div.setAttribute('attribute-code', item.code); + div.setAttribute('attribute-id', item.id); + div.innerHTML = input; + subDiv.setAttribute('aria-activedescendant', ''); + subDiv.setAttribute('tabindex', 0); + subDiv.setAttribute('aria-invalid', false); + subDiv.setAttribute('aria-required', true); + subDiv.setAttribute('role', 'listbox'); + subDiv.setAttributeNode(listLabel); + subDiv.setAttribute('class', classes.attributeOptionsWrapper + ' clearfix'); + subDiv.innerHTML = options + select; + + if ($widget.options.enableControlLabel) { + div.appendChild(firstSpan); + div.appendChild(secondSpan); + } + + div.appendChild(subDiv); + // Create new control - container.append( - '<div class="' + classes.attributeClass + ' ' + item.code + '" ' + - 'attribute-code="' + item.code + '" ' + - 'attribute-id="' + item.id + '">' + - label + - '<div aria-activedescendant="" ' + - 'tabindex="0" ' + - 'aria-invalid="false" ' + - 'aria-required="true" ' + - 'role="listbox" ' + listLabel + - 'class="' + classes.attributeOptionsWrapper + ' clearfix">' + - options + select + - '</div>' + input + - '</div>' - ); + container.append(div.outerHTML); $widget.optionsMap[item.id] = {}; @@ -493,7 +507,7 @@ define([ return ''; } - $.each(config.options, function () { + $.each(config.options, function (index) { var id, type, value, @@ -501,7 +515,8 @@ define([ label, width, height, - attr; + link, + div; if (!optionConfig.hasOwnProperty(this.id)) { return ''; @@ -509,7 +524,12 @@ define([ // Add more button if (moreLimit === countAttributes++) { - html += '<a href="#" class="' + moreClass + '">' + moreText + '</a>'; + link = document.createElement('a'); + link.setAttribute('class', moreClass); + link.setAttribute('href', '#'); + link.textContent = moreText; + + html += link.outerHTML; } id = this.id; @@ -519,48 +539,55 @@ define([ width = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.width : 110; height = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.height : 90; label = this.label ? this.label : ''; - attr = - ' id="' + controlId + '-item-' + id + '"' + - ' aria-checked="false"' + - ' aria-describedby="' + controlId + '"' + - ' tabindex="0"' + - ' option-type="' + type + '"' + - ' option-id="' + id + '"' + - ' option-label="' + label + '"' + - ' aria-label="' + label + '"' + - ' option-tooltip-thumb="' + thumb + '"' + - ' option-tooltip-value="' + value + '"' + - ' role="option"' + - ' thumb-width="' + width + '"' + - ' thumb-height="' + height + '"'; + + div = document.createElement('div'); + + div.setAttribute('id', controlId + '-item-' + id); + div.setAttribute('index', index); + div.setAttribute('aria-checked', false); + div.setAttribute('aria-describedby', controlId); + div.setAttribute('tabindex', 0); + div.setAttribute('option-type', type); + div.setAttribute('option-id', id); + div.setAttribute('option-label', label); + div.setAttribute('aria-label', label); + div.setAttribute('option-tooltip-thumb', thumb); + div.setAttribute('option-tooltip-value', value); + div.setAttribute('role', 'option'); + div.setAttribute('thumb-width', width); + div.setAttribute('thumb-height', height); if (!this.hasOwnProperty('products') || this.products.length <= 0) { - attr += ' option-empty="true"'; + div.setAttribute('option-empty', true); } if (type === 0) { // Text - html += '<div class="' + optionClass + ' text" ' + attr + '>' + (value ? value : label) + - '</div>'; + div.setAttribute('class', optionClass + ' text'); + div.textContent = value ? value : label; } else if (type === 1) { // Color - html += '<div class="' + optionClass + ' color" ' + attr + - ' style="background: ' + value + - ' no-repeat center; background-size: initial;">' + '' + - '</div>'; + div.setAttribute('class', optionClass + ' color'); + div.setAttribute('style', 'background: ' + value + ' no-repeat center; background-size: initial;'); + } else if (type === 2) { // Image - html += '<div class="' + optionClass + ' image" ' + attr + - ' style="background: url(' + value + ') no-repeat center; background-size: initial;width:' + - sizeConfig.swatchImage.width + 'px; height:' + sizeConfig.swatchImage.height + 'px">' + '' + - '</div>'; + div.setAttribute('class', optionClass + ' image'); + div.setAttribute('style', + 'background: url(' + value + + ') no-repeat center;' + + ' background-size: initial;' + + ' width:' + sizeConfig.swatchImage.width + 'px;' + + ' height:' + sizeConfig.swatchImage.height + 'px;'); } else if (type === 3) { // Clear - html += '<div class="' + optionClass + '" ' + attr + '></div>'; + div.setAttribute('class', optionClass); } else { // Default - html += '<div class="' + optionClass + '" ' + attr + '>' + label + '</div>'; + div.setAttribute('class', optionClass); + div.textContent = label; } + html += div.outerHTML; }); return html; @@ -575,30 +602,36 @@ define([ * @private */ _RenderSwatchSelect: function (config, chooseText) { - var html; + var select, + firstOption, + otherOption; if (this.options.jsonSwatchConfig.hasOwnProperty(config.id)) { return ''; } - html = - '<select class="' + this.options.classes.selectClass + ' ' + config.code + '">' + - '<option value="0" option-id="0">' + chooseText + '</option>'; + select = document.createElement('select'); + $(select).attr('class', this.options.classes.selectClass); + firstOption = document.createElement('option'); + $(firstOption).attr('value', 0); + $(firstOption).attr('option-id', 0); + $(firstOption).text(chooseText); + select.appendChild(firstOption); $.each(config.options, function () { - var label = this.label, - attr = ' value="' + this.id + '" option-id="' + this.id + '"'; + otherOption = document.createElement('option'); + $(otherOption).attr('value', this.id); + $(otherOption).attr('option-id', this.id); + $(otherOption).text(this.label); if (!this.hasOwnProperty('products') || this.products.length <= 0) { - attr += ' option-empty="true"'; + $(otherOption).attr('option-empty', true); } - html += '<option ' + attr + '>' + label + '</option>'; + select.appendChild(otherOption); }); - html += '</select>'; - - return html; + return select.outerHTML; }, /** @@ -695,7 +728,7 @@ define([ */ _sortImages: function (images) { return _.sortBy(images, function (image) { - return image.position; + return parseInt(image.position, 10); }); }, @@ -746,6 +779,12 @@ define([ $widget._UpdatePrice(); } + $(document).trigger('updateMsrpPriceBlock', + [ + parseInt($this.attr('index'), 10) + 1, + $widget.options.jsonConfig.optionPrices + ]); + $widget._loadMedia(eventName); $input.trigger('change'); }, @@ -918,7 +957,8 @@ define([ $productPrice = $product.find(this.options.selectorProductPrice), options = _.object(_.keys($widget.optionsMap), {}), result, - tierPriceHtml; + tierPriceHtml, + isShow; $widget.element.find('.' + $widget.options.classes.attributeClass + '[option-selected]').each(function () { var attributeId = $(this).attr('attribute-id'); @@ -935,11 +975,9 @@ define([ } ); - if (typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount) { - $(this.options.slyOldPriceSelector).show(); - } else { - $(this.options.slyOldPriceSelector).hide(); - } + isShow = typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount; + + $product.find(this.options.slyOldPriceSelector)[isShow ? 'show' : 'hide'](); if (typeof result != 'undefined' && result.tierPrices.length) { if (this.options.tierPriceTemplate) { diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php index 50e7437396b0c..7bb7d70f70229 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php @@ -126,7 +126,7 @@ protected function _prepareLayout() ) . '\', \'' . $this->getUrl( 'tax/*/delete', ['rate' => $rate] - ) . '\')', + ) . '\', {data: {}})', 'class' => 'delete' ] ); diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Rule/Delete.php b/app/code/Magento/Tax/Controller/Adminhtml/Rule/Delete.php index 71b6d7bf39396..45ad5acb7f033 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Rule/Delete.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Rule/Delete.php @@ -7,28 +7,34 @@ namespace Magento\Tax\Controller\Adminhtml\Rule; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\NotFoundException; class Delete extends \Magento\Tax\Controller\Adminhtml\Rule { /** * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $ruleId = (int)$this->getRequest()->getParam('rule'); try { $this->ruleService->deleteById($ruleId); - $this->messageManager->addSuccess(__('The tax rule has been deleted.')); + $this->messageManager->addSuccessMessage(__('The tax rule has been deleted.')); return $resultRedirect->setPath('tax/*/'); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - $this->messageManager->addError(__('This rule no longer exists.')); + $this->messageManager->addErrorMessage(__('This rule no longer exists.')); return $resultRedirect->setPath('tax/*/'); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError(__('Something went wrong deleting this tax rule.')); + $this->messageManager->addErrorMessage(__('Something went wrong deleting this tax rule.')); } return $resultRedirect->setUrl($this->_redirect->getRedirectUrl($this->getUrl('*'))); diff --git a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php index afcfa1bbebcb0..c543648d19ade 100644 --- a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php +++ b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php @@ -7,6 +7,9 @@ use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +/** + * Abstract aggregate calculator. + */ abstract class AbstractAggregateCalculator extends AbstractCalculator { /** @@ -106,11 +109,12 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ $rowTaxes = []; $rowTaxesBeforeDiscount = []; $appliedTaxes = []; + $rowTotalForTaxCalculation = $this->getPriceForTaxCalculation($item, $price) * $quantity; //Apply each tax rate separately foreach ($appliedRates as $appliedRate) { $taxId = $appliedRate['id']; $taxRate = $appliedRate['percent']; - $rowTaxPerRate = $this->calculationTool->calcTaxAmount($rowTotal, $taxRate, false, false); + $rowTaxPerRate = $this->calculationTool->calcTaxAmount($rowTotalForTaxCalculation, $taxRate, false, false); $deltaRoundingType = self::KEY_REGULAR_DELTA_ROUNDING; if ($applyTaxAfterDiscount) { $deltaRoundingType = self::KEY_TAX_BEFORE_DISCOUNT_DELTA_ROUNDING; @@ -121,7 +125,10 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ //Handle discount if ($applyTaxAfterDiscount) { //TODO: handle originalDiscountAmount - $taxableAmount = max($rowTotal - $discountAmount, 0); + $taxableAmount = max($rowTotalForTaxCalculation - $discountAmount, 0); + if ($taxableAmount && !$applyTaxAfterDiscount) { + $taxableAmount = $rowTotalForTaxCalculation; + } $rowTaxAfterDiscount = $this->calculationTool->calcTaxAmount( $taxableAmount, $taxRate, @@ -149,6 +156,7 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ $rowTaxBeforeDiscount = array_sum($rowTaxesBeforeDiscount); $rowTotalInclTax = $rowTotal + $rowTaxBeforeDiscount; $priceInclTax = $rowTotalInclTax / $quantity; + if ($round) { $priceInclTax = $this->calculationTool->round($priceInclTax); } @@ -167,6 +175,26 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ ->setAppliedTaxes($appliedTaxes); } + /** + * Get price for tax calculation. + * + * @param QuoteDetailsItemInterface $item + * @param float $price + * @return float + */ + private function getPriceForTaxCalculation(QuoteDetailsItemInterface $item, float $price) + { + if ($item->getExtensionAttributes() && $item->getExtensionAttributes()->getPriceForTaxCalculation()) { + $priceForTaxCalculation = $this->calculationTool->round( + $item->getExtensionAttributes()->getPriceForTaxCalculation() + ); + } else { + $priceForTaxCalculation = $price; + } + + return $priceForTaxCalculation; + } + /** * Round amount * diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php index ff31aa4ba90cb..65af4d7863830 100644 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -17,12 +17,17 @@ use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\Store\Model\Store; use Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory; use Magento\Tax\Api\Data\TaxClassKeyInterface; use Magento\Tax\Api\Data\TaxDetailsInterface; use Magento\Tax\Api\Data\TaxDetailsItemInterface; use Magento\Tax\Api\Data\QuoteDetailsInterface; use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterfaceFactory; /** * Tax totals calculation model @@ -131,6 +136,16 @@ class CommonTaxCollector extends AbstractTotal */ protected $quoteDetailsItemDataObjectFactory; + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var QuoteDetailsItemExtensionInterfaceFactory + */ + private $quoteDetailsItemExtensionFactory; + /** * Class constructor * @@ -141,6 +156,8 @@ class CommonTaxCollector extends AbstractTotal * @param \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory * @param CustomerAddressFactory $customerAddressFactory * @param CustomerAddressRegionFactory $customerAddressRegionFactory + * @param TaxHelper|null $taxHelper + * @param QuoteDetailsItemExtensionInterfaceFactory|null $quoteDetailsItemExtensionInterfaceFactory */ public function __construct( \Magento\Tax\Model\Config $taxConfig, @@ -149,7 +166,9 @@ public function __construct( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $quoteDetailsItemDataObjectFactory, \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory, CustomerAddressFactory $customerAddressFactory, - CustomerAddressRegionFactory $customerAddressRegionFactory + CustomerAddressRegionFactory $customerAddressRegionFactory, + TaxHelper $taxHelper = null, + QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null ) { $this->taxCalculationService = $taxCalculationService; $this->quoteDetailsDataObjectFactory = $quoteDetailsDataObjectFactory; @@ -158,6 +177,9 @@ public function __construct( $this->quoteDetailsItemDataObjectFactory = $quoteDetailsItemDataObjectFactory; $this->customerAddressFactory = $customerAddressFactory; $this->customerAddressRegionFactory = $customerAddressRegionFactory; + $this->taxHelper = $taxHelper ?: ObjectManager::getInstance()->get(TaxHelper::class); + $this->quoteDetailsItemExtensionFactory = $quoteDetailsItemExtensionInterfaceFactory ?: + ObjectManager::getInstance()->get(QuoteDetailsItemExtensionInterfaceFactory::class); } /** @@ -188,7 +210,7 @@ public function mapAddress(QuoteAddress $address) * @param bool $priceIncludesTax * @param bool $useBaseCurrency * @param string $parentCode - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @return QuoteDetailsItemInterface */ public function mapItem( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, @@ -201,7 +223,7 @@ public function mapItem( $sequence = 'sequence-' . $this->getNextIncrement(); $item->setTaxCalculationItemId($sequence); } - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $itemDataObjectFactory->create(); $itemDataObject->setCode($item->getTaxCalculationItemId()) ->setQuantity($item->getQty()) @@ -217,12 +239,28 @@ public function mapItem( if (!$item->getBaseTaxCalculationPrice()) { $item->setBaseTaxCalculationPrice($item->getBaseCalculationPriceOriginal()); } + + if ($this->taxHelper->applyTaxOnOriginalPrice()) { + $baseTaxCalculationPrice = $item->getBaseOriginalPrice(); + } else { + $baseTaxCalculationPrice = $item->getBaseCalculationPriceOriginal(); + } + $this->setPriceForTaxCalculation($itemDataObject, (float)$baseTaxCalculationPrice); + $itemDataObject->setUnitPrice($item->getBaseTaxCalculationPrice()) ->setDiscountAmount($item->getBaseDiscountAmount()); } else { if (!$item->getTaxCalculationPrice()) { $item->setTaxCalculationPrice($item->getCalculationPriceOriginal()); } + + if ($this->taxHelper->applyTaxOnOriginalPrice()) { + $taxCalculationPrice = $item->getOriginalPrice(); + } else { + $taxCalculationPrice = $item->getCalculationPriceOriginal(); + } + $this->setPriceForTaxCalculation($itemDataObject, (float)$taxCalculationPrice); + $itemDataObject->setUnitPrice($item->getTaxCalculationPrice()) ->setDiscountAmount($item->getDiscountAmount()); } @@ -232,6 +270,23 @@ public function mapItem( return $itemDataObject; } + /** + * Set price for tax calculation. + * + * @param QuoteDetailsItemInterface $quoteDetailsItem + * @param float $taxCalculationPrice + * @return void + */ + private function setPriceForTaxCalculation(QuoteDetailsItemInterface $quoteDetailsItem, float $taxCalculationPrice) + { + $extensionAttributes = $quoteDetailsItem->getExtensionAttributes(); + if (!$extensionAttributes) { + $extensionAttributes = $this->quoteDetailsItemExtensionFactory->create(); + } + $extensionAttributes->setPriceForTaxCalculation($taxCalculationPrice); + $quoteDetailsItem->setExtensionAttributes($extensionAttributes); + } + /** * Map item extra taxables * @@ -239,7 +294,7 @@ public function mapItem( * @param AbstractItem $item * @param bool $priceIncludesTax * @param bool $useBaseCurrency - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] + * @return QuoteDetailsItemInterface[] */ public function mapItemExtraTaxables( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, @@ -262,7 +317,7 @@ public function mapItemExtraTaxables( } else { $unitPrice = $extraTaxable[self::KEY_ASSOCIATED_TAXABLE_UNIT_PRICE]; } - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $itemDataObjectFactory->create(); $itemDataObject->setCode($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_CODE]) ->setType($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_TYPE]) @@ -285,9 +340,9 @@ public function mapItemExtraTaxables( * Add quote items * * @param ShippingAssignmentInterface $shippingAssignment - * @param bool $useBaseCurrency * @param bool $priceIncludesTax - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] + * @param bool $useBaseCurrency + * @return QuoteDetailsItemInterface[] */ public function mapItems( ShippingAssignmentInterface $shippingAssignment, @@ -295,7 +350,7 @@ public function mapItems( $useBaseCurrency ) { $items = $shippingAssignment->getItems(); - if (!count($items)) { + if (empty($items)) { return []; } @@ -358,10 +413,12 @@ public function populateAddressData(QuoteDetailsInterface $quoteDetails, QuoteAd } /** + * Get shipping data object. + * * @param ShippingAssignmentInterface $shippingAssignment * @param QuoteAddress\Total $total * @param bool $useBaseCurrency - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @return QuoteDetailsItemInterface */ public function getShippingDataObject( ShippingAssignmentInterface $shippingAssignment, @@ -376,7 +433,7 @@ public function getShippingDataObject( $total->setBaseShippingTaxCalculationAmount($total->getBaseShippingAmount()); } if ($total->getShippingTaxCalculationAmount() !== null) { - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $this->quoteDetailsItemDataObjectFactory->create() ->setType(self::ITEM_TYPE_SHIPPING) ->setCode(self::ITEM_CODE_SHIPPING) @@ -411,14 +468,14 @@ public function getShippingDataObject( * Populate QuoteDetails object from quote address object * * @param ShippingAssignmentInterface $shippingAssignment - * @param \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] $itemDataObjects + * @param QuoteDetailsItemInterface[] $itemDataObjects * @return \Magento\Tax\Api\Data\QuoteDetailsInterface */ protected function prepareQuoteDetails(ShippingAssignmentInterface $shippingAssignment, $itemDataObjects) { $items = $shippingAssignment->getItems(); $address = $shippingAssignment->getShipping()->getAddress(); - if (!count($items)) { + if (empty($items)) { return $this->quoteDetailsDataObjectFactory->create(); } @@ -540,6 +597,7 @@ protected function processProductItems( * Process applied taxes for items and quote * * @param QuoteAddress\Total $total + * @param ShippingAssignmentInterface $shippingAssignment * @param array $itemsByType * @return $this */ @@ -627,6 +685,9 @@ public function updateItemTaxInfo($quoteItem, $itemTaxDetails, $baseItemTaxDetai { //The price should be base price $quoteItem->setPrice($baseItemTaxDetails->getPrice()); + if ($quoteItem->getCustomPrice() && $this->taxHelper->applyTaxOnCustomPrice()) { + $quoteItem->setCustomPrice($baseItemTaxDetails->getPrice()); + } $quoteItem->setConvertedPrice($itemTaxDetails->getPrice()); $quoteItem->setPriceInclTax($itemTaxDetails->getPriceInclTax()); $quoteItem->setRowTotal($itemTaxDetails->getRowTotal()); @@ -837,8 +898,9 @@ protected function saveAppliedTaxes() } /** - * Increment and return counter. This function is intended to be used to generate temporary - * id for an item. + * Increment and return counter. + * + * This function is intended to be used to generate temporary id for an item. * * @return int */ diff --git a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php index 89800e3be872e..2a7eeb27ee07e 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php @@ -4,13 +4,12 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Tax\Test\Unit\Model\Calculation; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Tax\Model\Calculation\RowBaseCalculator; use Magento\Tax\Model\Calculation\TotalBaseCalculator; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -68,6 +67,11 @@ class RowBaseAndTotalBaseCalculatorTestCase extends \PHPUnit\Framework\TestCase */ protected $taxDetailsItem; + /** + * @var \Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteDetailsItemExtension; + /** * initialize all mocks * @@ -84,7 +88,10 @@ public function initMocks($isTaxIncluded) protected function setUp() { $this->objectManager = new ObjectManager($this); - $this->taxItemDetailsDataObjectFactory = $this->createPartialMock(\Magento\Tax\Api\Data\TaxDetailsItemInterfaceFactory::class, ['create']); + $this->taxItemDetailsDataObjectFactory = $this->createPartialMock( + \Magento\Tax\Api\Data\TaxDetailsItemInterfaceFactory::class, + ['create'] + ); $this->taxDetailsItem = $this->objectManager->getObject(\Magento\Tax\Model\TaxDetails\ItemDetails::class); $this->taxItemDetailsDataObjectFactory->expects($this->any()) ->method('create') @@ -100,11 +107,24 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->mockItem = $this->getMockBuilder(\Magento\Tax\Api\Data\QuoteDetailsItemInterface::class)->getMock(); - - $this->appliedTaxDataObjectFactory = $this->createPartialMock(\Magento\Tax\Api\Data\AppliedTaxInterfaceFactory::class, ['create']); + $this->mockItem = $this->getMockBuilder(\Magento\Tax\Api\Data\QuoteDetailsItemInterface::class) + ->disableOriginalConstructor()->setMethods(['getExtensionAttributes', 'getUnitPrice']) + ->getMockForAbstractClass(); + $this->quoteDetailsItemExtension = $this->getMockBuilder(QuoteDetailsItemExtensionInterface::class) + ->disableOriginalConstructor()->setMethods(['getPriceForTaxCalculation']) + ->getMockForAbstractClass(); + $this->mockItem->expects($this->any())->method('getExtensionAttributes') + ->willReturn($this->quoteDetailsItemExtension); + + $this->appliedTaxDataObjectFactory = $this->createPartialMock( + \Magento\Tax\Api\Data\AppliedTaxInterfaceFactory::class, + ['create'] + ); - $this->appliedTaxRateDataObjectFactory = $this->createPartialMock(\Magento\Tax\Api\Data\AppliedTaxRateInterfaceFactory::class, ['create']); + $this->appliedTaxRateDataObjectFactory = $this->createPartialMock( + \Magento\Tax\Api\Data\AppliedTaxRateInterfaceFactory::class, + ['create'] + ); $this->appliedTaxRate = $this->objectManager->getObject(\Magento\Tax\Model\TaxDetails\AppliedTaxRate::class); $this->appliedTaxRateDataObjectFactory->expects($this->any()) ->method('create') diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php index 9b963434e321d..8e0f9b8226f42 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php @@ -3,81 +3,108 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); // @codingStandardsIgnoreFile namespace Magento\Tax\Test\Unit\Model\Sales\Total\Quote; -/** - * Test class for \Magento\Tax\Model\Sales\Total\Quote\Tax - */ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Tax\Api\Data\TaxDetailsItemInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Store\Model\Store; +use Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector; +use Magento\Tax\Model\Config; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Model\Quote; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\TaxClassKeyInterface; +use Magento\Tax\Model\Sales\Quote\ItemDetails; +use Magento\Tax\Model\TaxClass\Key as TaxClassKey; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; +use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Api\Data\ShippingInterface; +use Magento\Quote\Model\Quote\Address\Total as QuoteAddressTotal; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** + * Common tax collector test + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CommonTaxCollectorTest extends \PHPUnit\Framework\TestCase +class CommonTaxCollectorTest extends TestCase { /** - * @var \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector + * @var CommonTaxCollector */ private $commonTaxCollector; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Tax\Model\Config + * @var MockObject|Config */ private $taxConfig; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Quote\Model\Quote\Address + * @var MockObject|QuoteAddress */ private $address; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Quote\Model\Quote + * @var MockObject|Quote */ private $quote; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\Store + * @var MockObject|Store */ private $store; /** - * @var \PHPUnit_Framework_MockObject_MockObject| + * @var MockObject */ protected $taxClassKeyDataObjectFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject| + * @var MockObject */ protected $quoteDetailsItemDataObjectFactoryMock; /** - * @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @var QuoteDetailsItemInterface */ protected $quoteDetailsItemDataObject; /** - * @var \Magento\Tax\Api\Data\TaxClassKeyInterface + * @var TaxClassKeyInterface */ protected $taxClassKeyDataObject; + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * {@inheritdoc} + */ protected function setUp() { $objectManager = new ObjectManager($this); - $this->taxConfig = $this->getMockBuilder(\Magento\Tax\Model\Config::class) + $this->taxConfig = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() - ->setMethods(['getShippingTaxClass', 'shippingPriceIncludesTax']) + ->setMethods(['getShippingTaxClass', 'shippingPriceIncludesTax', 'discountTax']) ->getMock(); - $this->store = $this->getMockBuilder(\Magento\Store\Model\Store::class) + $this->store = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->setMethods(['__wakeup']) ->getMock(); - $this->quote = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + $this->quote = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->setMethods(['__wakeup', 'getStore']) ->getMock(); @@ -86,7 +113,7 @@ protected function setUp() ->method('getStore') ->will($this->returnValue($this->store)); - $this->address = $this->getMockBuilder(\Magento\Quote\Model\Quote\Address::class) + $this->address = $this->getMockBuilder(QuoteAddress::class) ->disableOriginalConstructor() ->getMock(); @@ -94,35 +121,41 @@ protected function setUp() ->method('getQuote') ->will($this->returnValue($this->quote)); $methods = ['create']; - $this->quoteDetailsItemDataObject = $objectManager->getObject( - \Magento\Tax\Model\Sales\Quote\ItemDetails::class - ); - $this->taxClassKeyDataObject = $objectManager->getObject(\Magento\Tax\Model\TaxClass\Key::class); + $this->quoteDetailsItemDataObject = $objectManager->getObject(ItemDetails::class); + $this->taxClassKeyDataObject = $objectManager->getObject(TaxClassKey::class); $this->quoteDetailsItemDataObjectFactoryMock - = $this->createPartialMock(\Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory::class, $methods); + = $this->createPartialMock(QuoteDetailsItemInterfaceFactory::class, $methods); $this->quoteDetailsItemDataObjectFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->quoteDetailsItemDataObject); $this->taxClassKeyDataObjectFactoryMock = - $this->createPartialMock(\Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory::class, $methods); + $this->createPartialMock(TaxClassKeyInterfaceFactory::class, $methods); $this->taxClassKeyDataObjectFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->taxClassKeyDataObject); + $this->taxHelper = $this->getMockBuilder(TaxHelper::class) + ->disableOriginalConstructor() + ->getMock(); $this->commonTaxCollector = $objectManager->getObject( - \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector::class, + CommonTaxCollector::class, [ 'taxConfig' => $this->taxConfig, 'quoteDetailsItemDataObjectFactory' => $this->quoteDetailsItemDataObjectFactoryMock, - 'taxClassKeyDataObjectFactory' => $this->taxClassKeyDataObjectFactoryMock + 'taxClassKeyDataObjectFactory' => $this->taxClassKeyDataObjectFactoryMock, + 'taxHelper' => $this->taxHelper, ] ); } /** + * Test for GetShippingDataObject + * * @param array $addressData * @param bool $useBaseCurrency * @param string $shippingTaxClass * @param bool $shippingPriceInclTax + * + * @return void * @dataProvider getShippingDataObjectDataProvider */ public function testGetShippingDataObject( @@ -131,7 +164,7 @@ public function testGetShippingDataObject( $shippingTaxClass, $shippingPriceInclTax ) { - $shippingAssignmentMock = $this->createMock(\Magento\Quote\Api\Data\ShippingAssignmentInterface::class); + $shippingAssignmentMock = $this->createMock(ShippingAssignmentInterface::class); $methods = [ 'getShippingDiscountAmount', 'getShippingTaxCalculationAmount', @@ -141,8 +174,10 @@ public function testGetShippingDataObject( 'getBaseShippingAmount', 'getBaseShippingDiscountAmount' ]; - $totalsMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Total::class, $methods); - $shippingMock = $this->createMock(\Magento\Quote\Api\Data\ShippingInterface::class); + /** @var MockObject|QuoteAddressTotal $totalsMock */ + $totalsMock = $this->createPartialMock(QuoteAddressTotal::class, $methods); + $shippingMock = $this->createMock(ShippingInterface::class); + /** @var MockObject|ShippingAssignmentInterface $shippingAssignmentMock */ $shippingAssignmentMock->expects($this->once())->method('getShipping')->willReturn($shippingMock); $shippingMock->expects($this->once())->method('getAddress')->willReturn($this->address); $baseShippingAmount = $addressData['base_shipping_amount']; @@ -184,9 +219,44 @@ public function testGetShippingDataObject( } /** + * Update item tax info + * + * @return void + */ + public function testUpdateItemTaxInfo() + { + /** @var MockObject|QuoteItem $quoteItem */ + $quoteItem = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->setMethods(['getPrice', 'setPrice', 'getCustomPrice', 'setCustomPrice']) + ->getMock(); + $this->taxHelper->method('applyTaxOnCustomPrice')->willReturn(true); + $quoteItem->method('getCustomPrice')->willReturn(true); + /** @var MockObject|TaxDetailsItemInterface $itemTaxDetails */ + $itemTaxDetails = $this->getMockBuilder(TaxDetailsItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var MockObject|TaxDetailsItemInterface $baseItemTaxDetails */ + $baseItemTaxDetails = $this->getMockBuilder(TaxDetailsItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $quoteItem->expects($this->once())->method('setCustomPrice'); + + $this->commonTaxCollector->updateItemTaxInfo( + $quoteItem, + $itemTaxDetails, + $baseItemTaxDetails, + $this->store + ); + } + + /** + * Data for testGetShippingDataObject + * * @return array */ - public function getShippingDataObjectDataProvider() + public function getShippingDataObjectDataProvider(): array { $data = [ 'free_shipping' => [ diff --git a/app/code/Magento/Tax/composer.json b/app/code/Magento/Tax/composer.json index 52f636d5db077..be194636c1ec4 100644 --- a/app/code/Magento/Tax/composer.json +++ b/app/code/Magento/Tax/composer.json @@ -22,7 +22,7 @@ "magento/module-tax-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Tax/etc/di.xml b/app/code/Magento/Tax/etc/di.xml index 096f8359fadd3..3b46b0f9e258c 100644 --- a/app/code/Magento/Tax/etc/di.xml +++ b/app/code/Magento/Tax/etc/di.xml @@ -143,6 +143,7 @@ <arguments> <argument name="fieldMapping" xsi:type="array"> <item name="id" xsi:type="string">tax_calculation_rule_id</item> + <item name="code" xsi:type="string">main_table.code</item> <item name="tax_rate_ids" xsi:type="string">tax_calculation_rate_id</item> <item name="customer_tax_class_ids" xsi:type="string">cd.customer_tax_class_id</item> <item name="product_tax_class_ids" xsi:type="string">cd.product_tax_class_id</item> @@ -154,6 +155,7 @@ <arguments> <argument name="fieldMapping" xsi:type="array"> <item name="id" xsi:type="string">tax_calculation_rule_id</item> + <item name="code" xsi:type="string">main_table.code</item> <item name="tax_rate_ids" xsi:type="string">tax_calculation_rate_id</item> <item name="customer_tax_class_ids" xsi:type="string">cd.customer_tax_class_id</item> <item name="product_tax_class_ids" xsi:type="string">cd.product_tax_class_id</item> diff --git a/app/code/Magento/Tax/etc/extension_attributes.xml b/app/code/Magento/Tax/etc/extension_attributes.xml index 90a5e6d2ecee3..41af1df836d6f 100644 --- a/app/code/Magento/Tax/etc/extension_attributes.xml +++ b/app/code/Magento/Tax/etc/extension_attributes.xml @@ -20,4 +20,7 @@ <extension_attributes for="Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface"> <attribute code="tax_adjustments" type="Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface" /> </extension_attributes> + <extension_attributes for="Magento\Tax\Api\Data\QuoteDetailsItemInterface"> + <attribute code="price_for_tax_calculation" type="float" /> + </extension_attributes> </config> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/class/page/edit.phtml b/app/code/Magento/Tax/view/adminhtml/templates/class/page/edit.phtml deleted file mode 100644 index 18e86549a1ff9..0000000000000 --- a/app/code/Magento/Tax/view/adminhtml/templates/class/page/edit.phtml +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -?> -<div data-mage-init='{"floatingHeader": {}}' class="page-actions"> - <?= $block->getBackButtonHtml() ?> - <?= $block->getResetButtonHtml() ?> - <?= $block->getDeleteButtonHtml() ?> - <?= $block->getSaveButtonHtml() ?> -</div> -<?= $block->getRenameFormHtml() ?> -<script type="text/x-magento-init"> - { - "#<?= /* @escapeNotVerified */ $block->getRenameFormId() ?>": { - "Magento_Tax/js/page/validate": {} - } - } -</script> diff --git a/app/code/Magento/Tax/view/adminhtml/web/js/page/validate.js b/app/code/Magento/Tax/view/adminhtml/web/js/page/validate.js deleted file mode 100644 index a49f199ba56b6..0000000000000 --- a/app/code/Magento/Tax/view/adminhtml/web/js/page/validate.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'jquery', - 'mage/mage' -], function (jQuery) { - 'use strict'; - - return function (data, element) { - jQuery(element).mage('form').mage('validation'); - }; -}); diff --git a/app/code/Magento/Tax/view/frontend/web/js/view/checkout/summary/tax.js b/app/code/Magento/Tax/view/frontend/web/js/view/checkout/summary/tax.js index 39220ae0fac66..2b1f387f5c8c4 100644 --- a/app/code/Magento/Tax/view/frontend/web/js/view/checkout/summary/tax.js +++ b/app/code/Magento/Tax/view/frontend/web/js/view/checkout/summary/tax.js @@ -12,14 +12,16 @@ define([ 'Magento_Checkout/js/view/summary/abstract-total', 'Magento_Checkout/js/model/quote', 'Magento_Checkout/js/model/totals', - 'jquery', - 'mage/translate' -], function (ko, Component, quote, totals, $, $t) { + 'mage/translate', + 'underscore' +], function (ko, Component, quote, totals, $t, _) { 'use strict'; var isTaxDisplayedInGrandTotal = window.checkoutConfig.includeTaxInGrandTotal, isFullTaxSummaryDisplayed = window.checkoutConfig.isFullTaxSummaryDisplayed, - isZeroTaxDisplayed = window.checkoutConfig.isZeroTaxDisplayed; + isZeroTaxDisplayed = window.checkoutConfig.isZeroTaxDisplayed, + taxAmount = 0, + rates = 0; return Component.extend({ defaults: { @@ -67,7 +69,7 @@ define([ } } - return parseFloat(amount); + return amount; }, /** @@ -99,6 +101,33 @@ define([ return this.getFormattedPrice(amount); }, + /** + * @param {*} parent + * @param {*} percentage + * @return {*|String} + */ + getTaxAmount: function (parent, percentage) { + var totalPercentage = 0; + + taxAmount = parent.amount; + rates = parent.rates; + _.each(rates, function (rate) { + totalPercentage += parseFloat(rate.percent); + }); + + return this.getFormattedPrice(this.getPercentAmount(taxAmount, totalPercentage, percentage)); + }, + + /** + * @param {*} amount + * @param {*} totalPercentage + * @param {*} percentage + * @return {*|String} + */ + getPercentAmount: function (amount, totalPercentage, percentage) { + return parseFloat(amount * percentage / totalPercentage); + }, + /** * @return {Array} */ diff --git a/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/tax.html b/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/tax.html index 9c45e73db6fa4..45c468096abe1 100644 --- a/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/tax.html +++ b/app/code/Magento/Tax/view/frontend/web/template/checkout/cart/totals/tax.html @@ -32,18 +32,16 @@ <!-- ko if: !percent --> <th class="mark" scope="row" colspan="1" data-bind="text: title"></th> <!-- /ko --> - <!-- ko if: $index() == 0 --> - <td class="amount" rowspan="1"> - <!-- ko if: $parents[1].isCalculated() --> - <span class="price" - data-bind="text: $parents[1].formatPrice($parents[0].amount), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> - <!-- /ko --> - <!-- ko ifnot: $parents[1].isCalculated() --> - <span class="not-calculated" - data-bind="text: $parents[1].formatPrice($parents[0].amount), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> - <!-- /ko --> - </td> - <!-- /ko --> + <td class="amount" rowspan="1"> + <!-- ko if: $parents[1].isCalculated() --> + <span class="price" + data-bind="text: $parents[1].getTaxAmount($parents[0], percent), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> + <!-- /ko --> + <!-- ko ifnot: $parents[1].isCalculated() --> + <span class="not-calculated" + data-bind="text: $parents[1].getTaxAmount($parents[0], percent), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> + <!-- /ko --> + </td> </tr> <!-- /ko --> <!-- /ko --> diff --git a/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/tax.html b/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/tax.html index 0f2e3251bcfdb..5f1ac86e38ffd 100644 --- a/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/tax.html +++ b/app/code/Magento/Tax/view/frontend/web/template/checkout/summary/tax.html @@ -43,18 +43,16 @@ <!-- ko if: !percent --> <th class="mark" scope="row" data-bind="text: title"></th> <!-- /ko --> - <!-- ko if: $index() == 0 --> - <td class="amount"> - <!-- ko if: $parents[1].isCalculated() --> - <span class="price" - data-bind="text: $parents[1].formatPrice($parents[0].amount), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> - <!-- /ko --> - <!-- ko ifnot: $parents[1].isCalculated() --> - <span class="not-calculated" - data-bind="text: $parents[1].formatPrice($parents[0].amount), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> - <!-- /ko --> - </td> - <!-- /ko --> + <td class="amount"> + <!-- ko if: $parents[1].isCalculated() --> + <span class="price" + data-bind="text: $parents[1].getTaxAmount($parents[0], percent), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> + <!-- /ko --> + <!-- ko ifnot: $parents[1].isCalculated() --> + <span class="not-calculated" + data-bind="text: $parents[1].getTaxAmount($parents[0], percent), attr: {'data-th': title, 'rowspan': $parents[0].rates.length }"></span> + <!-- /ko --> + </td> </tr> <!-- /ko --> <!-- /ko --> diff --git a/app/code/Magento/Theme/Block/Adminhtml/System/Design/Theme/Edit.php b/app/code/Magento/Theme/Block/Adminhtml/System/Design/Theme/Edit.php index 749a9f8dafac5..0ebbc794b7acd 100644 --- a/app/code/Magento/Theme/Block/Adminhtml/System/Design/Theme/Edit.php +++ b/app/code/Magento/Theme/Block/Adminhtml/System/Design/Theme/Edit.php @@ -77,7 +77,7 @@ protected function _prepareLayout() if ($theme->hasChildThemes()) { $message = __('Are you sure you want to delete this theme?'); $onClick = sprintf( - "deleteConfirm('%s', '%s')", + "deleteConfirm('%s', '%s', {data: {}})", $message, $this->getUrl('adminhtml/*/delete', ['id' => $theme->getId()]) ); diff --git a/app/code/Magento/Theme/composer.json b/app/code/Magento/Theme/composer.json index a89fb10d9769d..e83b49bbbee03 100644 --- a/app/code/Magento/Theme/composer.json +++ b/app/code/Magento/Theme/composer.json @@ -22,7 +22,7 @@ "magento/module-directory": "100.2.*" }, "type": "magento2-module", - "version": "100.2.7", + "version": "100.2.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php index 9e43f1d11bed0..f7a1396c5e8c9 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php @@ -7,6 +7,7 @@ use Magento\Authorization\Model\UserContextInterface; use Magento\Backend\App\Action\Context; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Json\DecoderInterface; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Ui\Api\BookmarkManagementInterface; @@ -86,11 +87,16 @@ public function __construct( * Action for AJAX request * * @return void + * @throws NotFoundException + * @throws \InvalidArgumentException */ public function execute() { $bookmark = $this->bookmarkFactory->create(); $jsonData = $this->_request->getParam('data'); + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } if (!$jsonData) { throw new \InvalidArgumentException('Invalid parameter "data"'); } diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml index 8880f7c3e1cc7..3d4efa13ce3a0 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml @@ -7,10 +7,11 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminMessagesSection"> <element name="successMessage" type="text" selector=".message-success"/> <element name="errorMessage" type="text" selector=".message.message-error.error"/> <element name="warningMessage" type="text" selector=".message-warning"/> + <element name="noticeMessage" type="text" selector=".message-notice"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml index b07f2d356b9ea..2834c367f136c 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontMessagesSection"> <element name="successMessage" type="text" selector=".message-success"/> + <element name="errorMessage" type="text" selector=".message-error"/> </section> </sections> diff --git a/app/code/Magento/Ui/composer.json b/app/code/Magento/Ui/composer.json index effe010a79fe9..4ad83870331a4 100644 --- a/app/code/Magento/Ui/composer.json +++ b/app/code/Magento/Ui/composer.json @@ -13,7 +13,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "101.0.7", + "version": "101.0.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js index 547e6cde59839..b25bdc165ccba 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js @@ -18,16 +18,16 @@ define([ return Abstract.extend({ defaults: { elementSelector: 'textarea', - suffixRegExpPattern: '\\${ \\$.wysiwygUniqueSuffix }', + suffixRegExpPattern: '${ $.wysiwygUniqueSuffix }', $wysiwygEditorButton: '', links: { value: '${ $.provider }:${ $.dataScope }' }, template: 'ui/form/field', elementTmpl: 'ui/form/element/wysiwyg', - content: '', - showSpinner: false, - loading: false, + content: '', + showSpinner: false, + loading: false, listens: { disabled: 'setDisabled' } @@ -56,6 +56,7 @@ define([ initConfig: function (config) { var pattern = config.suffixRegExpPattern || this.constructor.defaults.suffixRegExpPattern; + pattern = pattern.replace(/\$/g, '\\$&'); config.content = config.content.replace(new RegExp(pattern, 'g'), this.getUniqueSuffix(config)); this._super(); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js index be7a1a13fbd61..c75f7797cf0f3 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js @@ -11,8 +11,9 @@ define([ 'mageUtils', 'uiRegistry', './column', - 'Magento_Ui/js/modal/confirm' -], function (_, utils, registry, Column, confirm) { + 'Magento_Ui/js/modal/confirm', + 'mage/dataPost' +], function (_, utils, registry, Column, confirm, dataPost) { 'use strict'; return Column.extend({ @@ -267,7 +268,14 @@ define([ * @param {Object} action - Action's data. */ defaultCallback: function (actionIndex, recordId, action) { - window.location.href = action.href; + if (action.post) { + dataPost().postData({ + action: action.href, + data: {} + }); + } else { + window.location.href = action.href; + } }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js index be825a391cf07..bb74b84541a57 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js @@ -11,8 +11,9 @@ define([ 'uiLayout', 'mage/translate', 'mageUtils', - 'uiElement' -], function (_, layout, $t, utils, Element) { + 'uiElement', + 'jquery' +], function (_, layout, $t, utils, Element, $) { 'use strict'; return Element.extend({ @@ -29,11 +30,13 @@ define([ tracks: { value: true, previews: true, - inputValue: true + inputValue: true, + focused: true }, imports: { inputValue: 'value', - updatePreview: 'value' + updatePreview: 'value', + focused: false }, exports: { value: '${ $.provider }:params.search' @@ -88,6 +91,18 @@ define([ return this; }, + /** + * Click To ScrollTop. + */ + scrollTo: function ($data) { + $('html, body').animate({ + scrollTop: 0 + }, 'slow', function () { + $data.focused = false; + $data.focused = true; + }); + }, + /** * Resets input value to the last applied state. * diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index cbfc0dae90dda..831f11976fb2f 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -919,12 +919,12 @@ define([ ], 'validate-per-page-value-list': [ function (value) { - var isValid = utils.isEmpty(value), + var isValid = true, values = value.split(','), i; - if (isValid) { - return true; + if (utils.isEmpty(value)) { + return isValid; } for (i = 0; i < values.length; i++) { diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js index 31362644d415a..b7488cf994028 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js @@ -48,6 +48,10 @@ define([ params : [params]; + if (typeof message === 'function') { + message = message.call(rule); + } + message = params.reduce(function (msg, param, idx) { return msg.replace(new RegExp('\\{' + idx + '\\}', 'g'), param); }, message); diff --git a/app/code/Magento/Ui/view/base/web/templates/form/field.html b/app/code/Magento/Ui/view/base/web/templates/form/field.html index 6a095b4da14ed..376f165279f5d 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/field.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/field.html @@ -8,8 +8,8 @@ visible="visible" css="$data.additionalClasses" attr="'data-index': index"> - <div class="admin__field-label" visible="$data.labelVisible"> - <label if="$data.label" attr="for: uid"> + <div class="admin__field-label" if="$data.label" visible="$data.labelVisible" > + <label attr="for: uid"> <span translate="label" attr="'data-config-scope': $data.scopeLabel" /> </label> </div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html index 13b82a93eca25..fcad729a95fbb 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html @@ -5,7 +5,7 @@ */ --> <div class="data-grid-search-control-wrap"> - <label class="data-grid-search-label" attr="title: $t('Search'), for: index"> + <label class="data-grid-search-label" attr="title: $t('Search'), for: index" data-bind="click: scrollTo"> <span translate="'Search'"/> </label> <input class="admin__control-text data-grid-search-control" type="text" @@ -16,6 +16,7 @@ placeholder: $t(placeholder) }, textInput: inputValue, + hasFocus: focused, keyboard: { 13: apply.bind($data, false), 27: cancel diff --git a/app/code/Magento/Ui/view/base/web/templates/group/group.html b/app/code/Magento/Ui/view/base/web/templates/group/group.html index e30ac7a377542..6d6e61b805d62 100644 --- a/app/code/Magento/Ui/view/base/web/templates/group/group.html +++ b/app/code/Magento/Ui/view/base/web/templates/group/group.html @@ -8,10 +8,11 @@ visible="visible" css="_required: required" attr="'data-index': index"> - <legend class="admin__field-label" if="showLabel"> - <span translate="label" attr="'data-config-scope': $data.scopeLabel"/> - </legend> - + <div if="showLabel" class="admin__field-label"> + <legend> + <span translate="label" attr="'data-config-scope': $data.scopeLabel"/> + </legend> + </div> <div class="admin__field-control" css="$data.additionalClasses"> <each args="elems"> <if args="visible()" if="!$data.additionalForGroup"> diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index a9e289d57300b..89ba9721ba1f2 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -3,9 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -// @codingStandardsIgnoreFile - namespace Magento\Ups\Model; use Magento\Framework\HTTP\ClientFactory; @@ -79,7 +76,7 @@ class Carrier extends AbstractCarrierOnline implements CarrierInterface * * @var string */ - protected $_defaultCgiGatewayUrl = 'http://www.ups.com:80/using/services/rave/qcostcgi.cgi'; + protected $_defaultCgiGatewayUrl = 'https://www.ups.com/using/services/rave/qcostcgi.cgi'; /** * Test urls for shipment @@ -323,8 +320,9 @@ public function setRequest(RateRequest $request) } //for UPS, puero rico state for US will assume as puerto rico country - if ($destCountry == self::USA_COUNTRY_ID && ($request->getDestPostcode() == '00912' || - $request->getDestRegionCode() == self::PUERTORICO_COUNTRY_ID) + if ($destCountry == self::USA_COUNTRY_ID + && ($request->getDestPostcode() == '00912' + || $request->getDestRegionCode() == self::PUERTORICO_COUNTRY_ID) ) { $destCountry = self::PUERTORICO_COUNTRY_ID; } @@ -335,9 +333,9 @@ public function setRequest(RateRequest $request) } // For UPS, Las Palmas and Santa Cruz de Tenerife will be represented by Canary Islands country - if ( - $destCountry == self::SPAIN_COUNTRY_ID && - ($request->getDestRegionCode() == self::LAS_PALMAS_REGION_ID || $request->getDestRegionCode() == self::SANTA_CRUZ_DE_TENERIFE_REGION_ID) + if ($destCountry == self::SPAIN_COUNTRY_ID + && ($request->getDestRegionCode() == self::LAS_PALMAS_REGION_ID + || $request->getDestRegionCode() == self::SANTA_CRUZ_DE_TENERIFE_REGION_ID) ) { $destCountry = self::CANARY_ISLANDS_COUNTRY_ID; } @@ -456,7 +454,7 @@ protected function _getCgiQuotes() { $rowRequest = $this->_rawRequest; if (self::USA_COUNTRY_ID == $rowRequest->getDestCountry()) { - $destPostal = substr($rowRequest->getDestPostal(), 0, 5); + $destPostal = substr((string)$rowRequest->getDestPostal(), 0, 5); } else { $destPostal = $rowRequest->getDestPostal(); } @@ -474,7 +472,7 @@ protected function _getCgiQuotes() '47_rate_chart' => $rowRequest->getPickup(), '48_container' => $rowRequest->getContainer(), '49_residential' => $rowRequest->getDestType(), - 'weight_std' => strtolower($rowRequest->getUnitMeasure()), + 'weight_std' => strtolower((string)$rowRequest->getUnitMeasure()), ]; $params['47_rate_chart'] = $params['47_rate_chart']['label']; @@ -488,14 +486,14 @@ protected function _getCgiQuotes() } $client = new \Zend_Http_Client(); $client->setUri($url); - $client->setConfig(['maxredirects' => 0, 'timeout' => 30]); + $client->setConfig(['maxredirects' => 2, 'timeout' => 30]); $client->setParameterGet($params); $response = $client->request(); $responseBody = $response->getBody(); $debugData['result'] = $responseBody; $this->_setCachedQuotes($params, $responseBody); - } catch (\Exception $e) { + } catch (\Throwable $e) { $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; $responseBody = ''; } @@ -538,7 +536,7 @@ protected function _parseCgiResponse($response) $priceArr = []; if (strlen(trim($response)) > 0) { $rRows = explode("\n", $response); - $allowedMethods = explode(",", $this->getConfigData('allowed_methods')); + $allowedMethods = explode(",", (string)$this->getConfigData('allowed_methods')); foreach ($rRows as $rRow) { $row = explode('%', $rRow); switch (substr($row[0], -1)) { @@ -614,7 +612,7 @@ protected function _getXmlQuotes() $rowRequest = $this->_rawRequest; if (self::USA_COUNTRY_ID == $rowRequest->getDestCountry()) { - $destPostal = substr($rowRequest->getDestPostal(), 0, 5); + $destPostal = substr((string)$rowRequest->getDestPostal(), 0, 5); } else { $destPostal = $rowRequest->getDestPostal(); } @@ -759,7 +757,7 @@ protected function _getXmlQuotes() $xmlResponse = $client->getBody(); $debugData['result'] = $xmlResponse; $this->_setCachedQuotes($xmlRequest, $xmlResponse); - } catch (\Exception $e) { + } catch (\Throwable $e) { $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; $xmlResponse = ''; } @@ -824,81 +822,30 @@ protected function _parseXmlResponse($xmlResponse) $success = (int)$arr[0]; if ($success === 1) { $arr = $xml->getXpath("//RatingServiceSelectionResponse/RatedShipment"); - $allowedMethods = explode(",", $this->getConfigData('allowed_methods')); + $allowedMethods = explode(",", (string)$this->getConfigData('allowed_methods')); // Negotiated rates $negotiatedArr = $xml->getXpath("//RatingServiceSelectionResponse/RatedShipment/NegotiatedRates"); - $negotiatedActive = $this->getConfigFlag( - 'negotiated_active' - ) && $this->getConfigData( - 'shipper_number' - ) && !empty($negotiatedArr); + $negotiatedActive = $this->getConfigFlag('negotiated_active') + && $this->getConfigData('shipper_number') + && !empty($negotiatedArr); $allowedCurrencies = $this->_currencyFactory->create()->getConfigAllowCurrencies(); + $errorTitle = ''; foreach ($arr as $shipElement) { - $code = (string)$shipElement->Service->Code; - if (in_array($code, $allowedMethods)) { - //The location of tax information is in a different place depending on whether we are using negotiated rates or not - if ($negotiatedActive) { - $includeTaxesArr = $xml->getXpath("//RatingServiceSelectionResponse/RatedShipment/NegotiatedRates/NetSummaryCharges/TotalChargesWithTaxes"); - $includeTaxesActive = $this->getConfigFlag( - 'include_taxes' - ) && !empty($includeTaxesArr); - if ($includeTaxesActive) { - $cost = $shipElement->NegotiatedRates->NetSummaryCharges->TotalChargesWithTaxes->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->NegotiatedRates->NetSummaryCharges->TotalChargesWithTaxes->CurrencyCode - ); - } - else { - $cost = $shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->CurrencyCode - ); - } - } else { - $includeTaxesArr = $xml->getXpath("//RatingServiceSelectionResponse/RatedShipment/TotalChargesWithTaxes"); - $includeTaxesActive = $this->getConfigFlag( - 'include_taxes' - ) && !empty($includeTaxesArr); - if ($includeTaxesActive) { - $cost = $shipElement->TotalChargesWithTaxes->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->TotalChargesWithTaxes->CurrencyCode - ); - } - else { - $cost = $shipElement->TotalCharges->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->TotalCharges->CurrencyCode - ); - } - } - - //convert price with Origin country currency code to base currency code - $successConversion = true; - if ($responseCurrencyCode) { - if (in_array($responseCurrencyCode, $allowedCurrencies)) { - $cost = (double)$cost * $this->_getBaseCurrencyRate($responseCurrencyCode); - } else { - $errorTitle = __( - 'We can\'t convert a rate from "%1-%2".', - $responseCurrencyCode, - $this->_request->getPackageCurrency()->getCode() - ); - $error = $this->_rateErrorFactory->create(); - $error->setCarrier('ups'); - $error->setCarrierTitle($this->getConfigData('title')); - $error->setErrorMessage($errorTitle); - $successConversion = false; - } - } - - if ($successConversion) { - $costArr[$code] = $cost; - $priceArr[$code] = $this->getMethodPrice((float)$cost, $code); - } - } + $this->processShippingRateForItem( + $shipElement, + $allowedMethods, + $allowedCurrencies, + $costArr, + $priceArr, + $negotiatedActive, + $xml, + $errorTitle + ); + } + if (empty($errorTitle)) { + unset($errorTitle); } } else { $arr = $xml->getXpath("//RatingServiceSelectionResponse/Response/Error/ErrorDescription/text()"); @@ -916,7 +863,7 @@ protected function _parseXmlResponse($xmlResponse) $error = $this->_rateErrorFactory->create(); $error->setCarrier('ups'); $error->setCarrierTitle($this->getConfigData('title')); - if ($this->getConfigData('specificerrmsg') !== '') { + if (!empty($this->getConfigData('specificerrmsg'))) { $errorTitle = $this->getConfigData('specificerrmsg'); } if (!isset($errorTitle)) { @@ -941,6 +888,102 @@ protected function _parseXmlResponse($xmlResponse) return $result; } + /** + * Processing rate for ship element + * + * @param \Magento\Framework\Simplexml\Element $shipElement + * @param array $allowedMethods + * @param array $allowedCurrencies + * @param array $costArr + * @param array $priceArr + * @param bool $negotiatedActive + * @param \Magento\Framework\Simplexml\Config $xml + * @param string $errorTitle + * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function processShippingRateForItem( + \Magento\Framework\Simplexml\Element $shipElement, + array $allowedMethods, + array $allowedCurrencies, + array &$costArr, + array &$priceArr, + bool $negotiatedActive, + \Magento\Framework\Simplexml\Config $xml, + string &$errorTitle + ) { + $code = (string)$shipElement->Service->Code; + if (in_array($code, $allowedMethods)) { + //The location of tax information is in a different place + // depending on whether we are using negotiated rates or not + if ($negotiatedActive) { + $includeTaxesArr = $xml->getXpath( + "//RatingServiceSelectionResponse/RatedShipment/NegotiatedRates" + . "/NetSummaryCharges/TotalChargesWithTaxes" + ); + $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); + if ($includeTaxesActive) { + $cost = $shipElement->NegotiatedRates + ->NetSummaryCharges + ->TotalChargesWithTaxes + ->MonetaryValue; + + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->NegotiatedRates + ->NetSummaryCharges + ->TotalChargesWithTaxes + ->CurrencyCode + ); + } else { + $cost = $shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->MonetaryValue; + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->CurrencyCode + ); + } + } else { + $includeTaxesArr = $xml->getXpath( + "//RatingServiceSelectionResponse/RatedShipment/TotalChargesWithTaxes" + ); + $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); + if ($includeTaxesActive) { + $cost = $shipElement->TotalChargesWithTaxes->MonetaryValue; + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->TotalChargesWithTaxes->CurrencyCode + ); + } else { + $cost = $shipElement->TotalCharges->MonetaryValue; + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->TotalCharges->CurrencyCode + ); + } + } + + //convert price with Origin country currency code to base currency code + $successConversion = true; + if ($responseCurrencyCode) { + if (in_array($responseCurrencyCode, $allowedCurrencies)) { + $cost = (double)$cost * $this->_getBaseCurrencyRate($responseCurrencyCode); + } else { + $errorTitle = __( + 'We can\'t convert a rate from "%1-%2".', + $responseCurrencyCode, + $this->_request->getPackageCurrency()->getCode() + ); + $error = $this->_rateErrorFactory->create(); + $error->setCarrier('ups'); + $error->setCarrierTitle($this->getConfigData('title')); + $error->setErrorMessage($errorTitle); + $successConversion = false; + } + } + + if ($successConversion) { + $costArr[$code] = $cost; + $priceArr[$code] = $this->getMethodPrice((float)$cost, $code); + } + } + } + /** * Get tracking * @@ -1025,7 +1068,6 @@ protected function _getXmlTracking($trackings) $url = $this->getConfigData('tracking_xml_url'); foreach ($trackings as $tracking) { - /** * RequestOption==>'1' to request all activities */ @@ -1040,13 +1082,13 @@ protected function _getXmlTracking($trackings) <IncludeFreight>01</IncludeFreight> </TrackRequest> XMLAuth; - $debugData['request'] = parent::filterDebugData($this->_xmlAccessRequest) . $xmlRequest; + $debugData['request'] = $this->filterDebugData($this->_xmlAccessRequest) . $xmlRequest; try { $client = $this->httpClientFactory->create(); $client->post($url, $this->_xmlAccessRequest . $xmlRequest); $xmlResponse = $client->getBody(); $debugData['result'] = $xmlResponse; - } catch (\Exception $e) { + } catch (\Throwable $e) { $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; $xmlResponse = ''; } @@ -1071,7 +1113,6 @@ protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) { $errorTitle = 'For some reason we can\'t retrieve tracking info right now.'; $resultArr = []; - $packageProgress = []; if ($xmlResponse) { $xml = new \Magento\Framework\Simplexml\Config(); @@ -1097,57 +1138,11 @@ protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) $activityTags = $xml->getXpath("//TrackResponse/Shipment/Package/Activity"); if ($activityTags) { $index = 1; + $resultArr['progressdetail'] = []; foreach ($activityTags as $activityTag) { - $addressArr = []; - if (isset($activityTag->ActivityLocation->Address->City)) { - $addressArr[] = (string)$activityTag->ActivityLocation->Address->City; - } - if (isset($activityTag->ActivityLocation->Address->StateProvinceCode)) { - $addressArr[] = (string)$activityTag->ActivityLocation->Address->StateProvinceCode; - } - if (isset($activityTag->ActivityLocation->Address->CountryCode)) { - $addressArr[] = (string)$activityTag->ActivityLocation->Address->CountryCode; - } - $dateArr = []; - $date = (string)$activityTag->Date; - //YYYYMMDD - $dateArr[] = substr($date, 0, 4); - $dateArr[] = substr($date, 4, 2); - $dateArr[] = substr($date, -2, 2); - - $timeArr = []; - $time = (string)$activityTag->Time; - //HHMMSS - $timeArr[] = substr($time, 0, 2); - $timeArr[] = substr($time, 2, 2); - $timeArr[] = substr($time, -2, 2); - - if ($index === 1) { - $resultArr['status'] = (string)$activityTag->Status->StatusType->Description; - $resultArr['deliverydate'] = implode('-', $dateArr); - //YYYY-MM-DD - $resultArr['deliverytime'] = implode(':', $timeArr); - //HH:MM:SS - $resultArr['deliverylocation'] = (string)$activityTag->ActivityLocation->Description; - $resultArr['signedby'] = (string)$activityTag->ActivityLocation->SignedForByName; - if ($addressArr) { - $resultArr['deliveryto'] = implode(', ', $addressArr); - } - } else { - $tempArr = []; - $tempArr['activity'] = (string)$activityTag->Status->StatusType->Description; - $tempArr['deliverydate'] = implode('-', $dateArr); - //YYYY-MM-DD - $tempArr['deliverytime'] = implode(':', $timeArr); - //HH:MM:SS - if ($addressArr) { - $tempArr['deliverylocation'] = implode(', ', $addressArr); - } - $packageProgress[] = $tempArr; - } + $this->processActivityTagInfo($activityTag, $index, $resultArr); $index++; } - $resultArr['progressdetail'] = $packageProgress; } } else { $arr = $xml->getXpath("//TrackResponse/Response/Error/ErrorDescription/text()"); @@ -1178,6 +1173,68 @@ protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) return $this->_result; } + /** + * Process tracking info from activity tag + * + * @param \Magento\Framework\Simplexml\Element $activityTag + * @param int $index + * @param array $resultArr + * @return void + */ + private function processActivityTagInfo( + \Magento\Framework\Simplexml\Element $activityTag, + int $index, + array &$resultArr + ) { + $addressArr = []; + if (isset($activityTag->ActivityLocation->Address->City)) { + $addressArr[] = (string)$activityTag->ActivityLocation->Address->City; + } + if (isset($activityTag->ActivityLocation->Address->StateProvinceCode)) { + $addressArr[] = (string)$activityTag->ActivityLocation->Address->StateProvinceCode; + } + if (isset($activityTag->ActivityLocation->Address->CountryCode)) { + $addressArr[] = (string)$activityTag->ActivityLocation->Address->CountryCode; + } + $dateArr = []; + $date = (string)$activityTag->Date; + //YYYYMMDD + $dateArr[] = substr($date, 0, 4); + $dateArr[] = substr($date, 4, 2); + $dateArr[] = substr($date, -2, 2); + + $timeArr = []; + $time = (string)$activityTag->Time; + //HHMMSS + $timeArr[] = substr($time, 0, 2); + $timeArr[] = substr($time, 2, 2); + $timeArr[] = substr($time, -2, 2); + + if ($index === 1) { + $resultArr['status'] = (string)$activityTag->Status->StatusType->Description; + $resultArr['deliverydate'] = implode('-', $dateArr); + //YYYY-MM-DD + $resultArr['deliverytime'] = implode(':', $timeArr); + //HH:MM:SS + $resultArr['deliverylocation'] = (string)$activityTag->ActivityLocation->Description; + $resultArr['signedby'] = (string)$activityTag->ActivityLocation->SignedForByName; + if ($addressArr) { + $resultArr['deliveryto'] = implode(', ', $addressArr); + } + } else { + $tempArr = []; + $tempArr['activity'] = (string)$activityTag->Status->StatusType->Description; + $tempArr['deliverydate'] = implode('-', $dateArr); + //YYYY-MM-DD + $tempArr['deliverytime'] = implode(':', $timeArr); + //HH:MM:SS + if ($addressArr) { + $tempArr['deliverylocation'] = implode(', ', $addressArr); + } + $resultArr['progressdetail'][] = $tempArr; + } + } + /** * Get tracking response * @@ -1215,7 +1272,7 @@ public function getResponse() */ public function getAllowedMethods() { - $allowed = explode(',', $this->getConfigData('allowed_methods')); + $allowed = explode(',', (string)$this->getConfigData('allowed_methods')); $arr = []; $isByCode = $this->getConfigData('type') == 'UPS_XML'; foreach ($allowed as $code) { @@ -1374,11 +1431,8 @@ protected function _formShipmentRequest(\Magento\Framework\DataObject $request) } // ups support reference number only for domestic service - if ($this->_isUSCountry( - $request->getRecipientAddressCountryCode() - ) && $this->_isUSCountry( - $request->getShipperAddressCountryCode() - ) + if ($this->_isUSCountry($request->getRecipientAddressCountryCode()) + && $this->_isUSCountry($request->getShipperAddressCountryCode()) ) { if ($request->getReferenceData()) { $referenceData = $request->getReferenceData() . $request->getPackageId(); @@ -1407,7 +1461,7 @@ protected function _formShipmentRequest(\Magento\Framework\DataObject $request) default: break; } - if (!is_null($serviceOptionsNode)) { + if ($serviceOptionsNode !== null) { $serviceOptionsNode->addChild( 'DeliveryConfirmation' )->addChild( @@ -1422,10 +1476,10 @@ protected function _formShipmentRequest(\Magento\Framework\DataObject $request) ->addChild('BillShipper') ->addChild('AccountNumber', $this->getConfigData('shipper_number')); - if ($request->getPackagingType() != $this->configHelper->getCode('container', 'ULE') && - $request->getShipperAddressCountryCode() == self::USA_COUNTRY_ID && - ($request->getRecipientAddressCountryCode() == 'CA' || - $request->getRecipientAddressCountryCode() == 'PR') + if ($request->getPackagingType() != $this->configHelper->getCode('container', 'ULE') + && $request->getShipperAddressCountryCode() == self::USA_COUNTRY_ID + && ($request->getRecipientAddressCountryCode() == 'CA' + || $request->getRecipientAddressCountryCode() == 'PR') ) { $invoiceLineTotalPart = $shipmentPart->addChild('InvoiceLineTotal'); $invoiceLineTotalPart->addChild('CurrencyCode', $request->getBaseCurrencyCode()); @@ -1453,7 +1507,7 @@ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) $request = $xmlRequest->addChild('Request'); $request->addChild('RequestAction', 'ShipAccept'); $xmlRequest->addChild('ShipmentDigest', $shipmentConfirmResponse->ShipmentDigest); - $debugData = ['request' => parent::filterDebugData($this->_xmlAccessRequest) . $xmlRequest->asXML()]; + $debugData = ['request' => $this->filterDebugData($this->_xmlAccessRequest) . $xmlRequest->asXML()]; try { $client = $this->httpClientFactory->create(); @@ -1461,14 +1515,14 @@ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) $xmlResponse = $client->getBody(); $debugData['result'] = $xmlResponse; $this->_setCachedQuotes($xmlRequest, $xmlResponse); - } catch (\Exception $e) { + } catch (\Throwable $e) { $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; $xmlResponse = ''; } try { $response = $this->_xmlElFactory->create(['data' => $xmlResponse]); - } catch (\Exception $e) { + } catch (\Throwable $e) { $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; } @@ -1479,6 +1533,7 @@ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) $shippingLabelContent = (string)$response->ShipmentResults->PackageResults->LabelImage->GraphicImage; $trackingNumber = (string)$response->ShipmentResults->PackageResults->TrackingNumber; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $result->setShippingLabelContent(base64_decode($shippingLabelContent)); $result->setTrackingNumber($trackingNumber); } @@ -1509,7 +1564,6 @@ public function getShipAcceptUrl() * * @param \Magento\Framework\DataObject $request * @return \Magento\Framework\DataObject - * @throws \Exception */ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) { @@ -1521,7 +1575,7 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) $xmlResponse = $this->_getCachedQuotes($xmlRequest); if ($xmlResponse === null) { - $debugData['request'] = parent::filterDebugData($this->_xmlAccessRequest) . $rawXmlRequest; + $debugData['request'] = $this->filterDebugData($this->_xmlAccessRequest) . $rawXmlRequest; $url = $this->getShipConfirmUrl(); $client = $this->httpClientFactory->create(); try { @@ -1529,23 +1583,20 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) $xmlResponse = $client->getBody(); $debugData['result'] = $xmlResponse; $this->_setCachedQuotes($xmlRequest, $xmlResponse); - } catch (\Exception $e) { + } catch (\Throwable $e) { $debugData['result'] = ['code' => $e->getCode(), 'error' => $e->getMessage()]; } } try { $response = $this->_xmlElFactory->create(['data' => $xmlResponse]); - } catch (\Exception $e) { + } catch (\Throwable $e) { $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; $result->setErrors($e->getMessage()); } if (isset($response->Response->Error) - && in_array( - $response->Response->Error->ErrorSeverity, - ['Hard', 'Transient'] - ) + && in_array($response->Response->Error->ErrorSeverity, ['Hard', 'Transient']) ) { $result->setErrors((string)$response->Response->Error->ErrorDescription); } @@ -1613,20 +1664,20 @@ public function getContainerTypes(\Magento\Framework\DataObject $params = null) ]; } $containerTypes = $containerTypes + [ - '03' => __('UPS Tube'), - '04' => __('PAK'), - '2a' => __('Small Express Box'), - '2b' => __('Medium Express Box'), - '2c' => __('Large Express Box'), - ]; + '03' => __('UPS Tube'), + '04' => __('PAK'), + '2a' => __('Small Express Box'), + '2b' => __('Medium Express Box'), + '2c' => __('Large Express Box'), + ]; } return ['00' => __('Customer Packaging')] + $containerTypes; - } elseif ($countryShipper == self::USA_COUNTRY_ID && - $countryRecipient == self::PUERTORICO_COUNTRY_ID && - ($method == '03' || - $method == '02' || - $method == '01') + } elseif ($countryShipper == self::USA_COUNTRY_ID + && $countryRecipient == self::PUERTORICO_COUNTRY_ID + && ($method == '03' + || $method == '02' + || $method == '01') ) { // Container types should be the same as for domestic $params->setCountryRecipient(self::USA_COUNTRY_ID); @@ -1713,6 +1764,7 @@ public function getCustomizableContainerTypes() /** * Get delivery confirmation level based on origin/destination + * * Return null if delivery confirmation is not acceptable * * @param string|null $countyDestination @@ -1720,7 +1772,7 @@ public function getCustomizableContainerTypes() */ protected function _getDeliveryConfirmationLevel($countyDestination = null) { - if (is_null($countyDestination)) { + if ($countyDestination === null) { return null; } diff --git a/app/code/Magento/Ups/composer.json b/app/code/Magento/Ups/composer.json index 9173ccbc3393b..8a124a7c10f80 100644 --- a/app/code/Magento/Ups/composer.json +++ b/app/code/Magento/Ups/composer.json @@ -16,7 +16,7 @@ "magento/module-config": "101.0.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Ups/etc/config.xml b/app/code/Magento/Ups/etc/config.xml index e2ac1c6d6c443..73b10dd5ff41b 100644 --- a/app/code/Magento/Ups/etc/config.xml +++ b/app/code/Magento/Ups/etc/config.xml @@ -19,7 +19,7 @@ <cutoff_cost /> <dest_type>RES</dest_type> <free_method>GND</free_method> - <gateway_url>http://www.ups.com/using/services/rave/qcostcgi.cgi</gateway_url> + <gateway_url>https://www.ups.com/using/services/rave/qcostcgi.cgi</gateway_url> <gateway_xml_url>https://onlinetools.ups.com/ups.app/xml/Rate</gateway_xml_url> <handling>0</handling> <model>Magento\Ups\Model\Carrier</model> @@ -37,7 +37,7 @@ <negotiated_active>0</negotiated_active> <include_taxes>0</include_taxes> <mode_xml>1</mode_xml> - <type>UPS</type> + <type>UPS_XML</type> <is_account_live>0</is_account_live> <active_rma>0</active_rma> <is_online>1</is_online> diff --git a/app/code/Magento/UrlRewrite/Block/Edit.php b/app/code/Magento/UrlRewrite/Block/Edit.php index baee8af893083..3c823fb56e1d7 100644 --- a/app/code/Magento/UrlRewrite/Block/Edit.php +++ b/app/code/Magento/UrlRewrite/Block/Edit.php @@ -65,7 +65,7 @@ public function __construct( */ protected function _prepareLayout() { - $this->setTemplate('edit.phtml'); + $this->setTemplate('Magento_UrlRewrite::edit.phtml'); $this->_addBackButton(); $this->_prepareLayoutFeatures(); @@ -173,7 +173,7 @@ protected function _addDeleteButton() ['id' => $this->getUrlRewrite()->getId()] ) ) - . ')', + . ', {data: {}})', 'class' => 'scalable delete', 'level' => -1 ] diff --git a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Delete.php b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Delete.php index f8f7b145e2806..8a558d70e84fa 100644 --- a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Delete.php +++ b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Delete.php @@ -6,21 +6,29 @@ */ namespace Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite; +use Magento\Framework\Exception\NotFoundException; + class Delete extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite { /** * URL rewrite delete action * * @return void + * @throws NotFoundException */ public function execute() { - if ($this->_getUrlRewrite()->getId()) { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } + + $id = (int)$this->_getUrlRewrite()->getId(); + if ($id) { try { $this->_getUrlRewrite()->delete(); - $this->messageManager->addSuccess(__('You deleted the URL rewrite.')); + $this->messageManager->addSuccessMessage(__('You deleted the URL rewrite.')); } catch (\Exception $e) { - $this->messageManager->addException($e, __('We can\'t delete URL Rewrite right now.')); + $this->messageManager->addExceptionMessage($e, __('We can\'t delete URL Rewrite right now.')); $this->_redirect('adminhtml/*/edit/', ['id' => $this->_getUrlRewrite()->getId()]); return; } diff --git a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php index 60b845f95e5cc..f98801c266109 100644 --- a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php +++ b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php @@ -14,6 +14,9 @@ use Psr\Log\LoggerInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteData; +/** + * DB storage implementation for url rewrites + */ class DbStorage extends AbstractStorage { /** @@ -37,7 +40,7 @@ class DbStorage extends AbstractStorage protected $resource; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ private $logger; @@ -45,7 +48,7 @@ class DbStorage extends AbstractStorage * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory $urlRewriteFactory * @param DataObjectHelper $dataObjectHelper * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Psr\Log\LoggerInterface|null $logger + * @param LoggerInterface|null $logger */ public function __construct( UrlRewriteFactory $urlRewriteFactory, @@ -56,7 +59,7 @@ public function __construct( $this->connection = $resource->getConnection(); $this->resource = $resource; $this->logger = $logger ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Psr\Log\LoggerInterface::class); + ->get(LoggerInterface::class); parent::__construct($urlRewriteFactory, $dataObjectHelper); } @@ -97,42 +100,18 @@ protected function doFindOneByData(array $data) $result = null; $requestPath = $data[UrlRewrite::REQUEST_PATH]; - - $data[UrlRewrite::REQUEST_PATH] = [ + $decodedRequestPath = urldecode($requestPath); + $data[UrlRewrite::REQUEST_PATH] = array_unique([ rtrim($requestPath, '/'), rtrim($requestPath, '/') . '/', - ]; + rtrim($decodedRequestPath, '/'), + rtrim($decodedRequestPath, '/') . '/', + ]); $resultsFromDb = $this->connection->fetchAll($this->prepareSelect($data)); - - if (count($resultsFromDb) === 1) { - $resultFromDb = current($resultsFromDb); - $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; - - // If request path matches the DB value or it's redirect - we can return result from DB - $canReturnResultFromDb = ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath - || in_array((int)$resultFromDb[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true)); - - // Otherwise return 301 redirect to request path from DB results - $result = $canReturnResultFromDb ? $resultFromDb : [ - UrlRewrite::ENTITY_TYPE => 'custom', - UrlRewrite::ENTITY_ID => '0', - UrlRewrite::REQUEST_PATH => $requestPath, - UrlRewrite::TARGET_PATH => $resultFromDb[UrlRewrite::REQUEST_PATH], - UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, - UrlRewrite::STORE_ID => $resultFromDb[UrlRewrite::STORE_ID], - UrlRewrite::DESCRIPTION => null, - UrlRewrite::IS_AUTOGENERATED => '0', - UrlRewrite::METADATA => null, - ]; - } else { - // If we have 2 results - return the row that matches request path - foreach ($resultsFromDb as $resultFromDb) { - if ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath) { - $result = $resultFromDb; - break; - } - } + if ($resultsFromDb) { + $urlRewrite = $this->extractMostRelevantUrlRewrite($requestPath, $resultsFromDb); + $result = $this->prepareUrlRewrite($requestPath, $urlRewrite); } return $result; @@ -142,8 +121,78 @@ protected function doFindOneByData(array $data) } /** - * @param UrlRewrite[] $urls + * Extract most relevant url rewrite from url rewrites list + * + * @param string $requestPath + * @param array $urlRewrites + * @return array|null + */ + private function extractMostRelevantUrlRewrite(string $requestPath, array $urlRewrites) + { + $prioritizedUrlRewrites = []; + foreach ($urlRewrites as $urlRewrite) { + switch (true) { + case $urlRewrite[UrlRewrite::REQUEST_PATH] === $requestPath: + $priority = 1; + break; + case $urlRewrite[UrlRewrite::REQUEST_PATH] === urldecode($requestPath): + $priority = 2; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim($requestPath, '/'): + $priority = 3; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim(urldecode($requestPath), '/'): + $priority = 4; + break; + default: + $priority = 5; + break; + } + $prioritizedUrlRewrites[$priority] = $urlRewrite; + } + ksort($prioritizedUrlRewrites); + + return array_shift($prioritizedUrlRewrites); + } + + /** + * Prepare url rewrite * + * If request path matches the DB value or it's redirect - we can return result from DB + * Otherwise return 301 redirect to request path from DB results + * + * @param string $requestPath + * @param array $urlRewrite + * @return array + */ + private function prepareUrlRewrite(string $requestPath, array $urlRewrite): array + { + $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; + $canReturnResultFromDb = ( + in_array($urlRewrite[UrlRewrite::REQUEST_PATH], [$requestPath, urldecode($requestPath)], true) + || in_array((int) $urlRewrite[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true) + ); + if (!$canReturnResultFromDb) { + $urlRewrite = [ + UrlRewrite::ENTITY_TYPE => 'custom', + UrlRewrite::ENTITY_ID => '0', + UrlRewrite::REQUEST_PATH => $requestPath, + UrlRewrite::TARGET_PATH => $urlRewrite[UrlRewrite::REQUEST_PATH], + UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, + UrlRewrite::STORE_ID => $urlRewrite[UrlRewrite::STORE_ID], + UrlRewrite::DESCRIPTION => null, + UrlRewrite::IS_AUTOGENERATED => '0', + UrlRewrite::METADATA => null, + ]; + } + + return $urlRewrite; + } + + /** + * Delete old url rewrites + * + * @param UrlRewrite[] $urls * @return void */ private function deleteOldUrls(array $urls) diff --git a/app/code/Magento/UrlRewrite/composer.json b/app/code/Magento/UrlRewrite/composer.json index 6247c47182804..82d0ce9cd8365 100644 --- a/app/code/Magento/UrlRewrite/composer.json +++ b/app/code/Magento/UrlRewrite/composer.json @@ -12,7 +12,7 @@ "magento/module-cms-url-rewrite": "100.2.*" }, "type": "magento2-module", - "version": "101.0.6", + "version": "101.0.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml b/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml index 7d8151d270308..de8575178d06d 100644 --- a/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml +++ b/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml @@ -14,6 +14,8 @@ <argument name="id" xsi:type="string">urlrewriteGrid</argument> <argument name="dataSource" xsi:type="object">Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection</argument> <argument name="default_sort" xsi:type="string">url_rewrite_id</argument> + <!-- Add below argument to save session parameter in URL rewrite grid --> + <argument name="save_parameters_in_session" xsi:type="string">1</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" as="grid.columnSet" name="adminhtml.url_rewrite.grid.columnSet"> <arguments> diff --git a/app/code/Magento/User/Block/Buttons.php b/app/code/Magento/User/Block/Buttons.php index bb7375ae83277..60cdf3341d8a6 100644 --- a/app/code/Magento/User/Block/Buttons.php +++ b/app/code/Magento/User/Block/Buttons.php @@ -34,6 +34,7 @@ public function __construct( /** * @return $this + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _prepareLayout() { @@ -64,7 +65,7 @@ protected function _prepareLayout() ) . '\', \'' . $this->getUrl( '*/*/delete', ['rid' => $this->getRequest()->getParam('rid')] - ) . '\')', + ) . '\', {data: {}})', 'class' => 'delete' ] ); @@ -110,6 +111,7 @@ public function getSaveButtonHtml() /** * @return string|void + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getDeleteButtonHtml() { diff --git a/app/code/Magento/User/Block/User/Edit.php b/app/code/Magento/User/Block/User/Edit.php index 6e036cf20fa25..7f435829df30f 100644 --- a/app/code/Magento/User/Block/User/Edit.php +++ b/app/code/Magento/User/Block/User/Edit.php @@ -73,7 +73,7 @@ protected function _construct() 'label' => __('Force Sign-In'), 'class' => 'invalidate-token', 'onclick' => "deleteConfirm('" . $this->escapeJs($this->escapeHtml($deleteConfirmMsg)) . - "', '" . $this->getInvalidateUrl() . "')", + "', '" . $this->getInvalidateUrl() . "', {data: {}})", ] ); } diff --git a/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php b/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php index 7aa0d2368f67b..9019f4b3c7009 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php @@ -9,6 +9,7 @@ use Magento\Authorization\Model\Acl\Role\Group as RoleGroup; use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Exception\State\UserLockedException; use Magento\Security\Model\SecurityCookie; use Magento\Framework\Exception\LocalizedException; @@ -68,9 +69,13 @@ private function getSecurityCookie() * Role form submit action to save or create new role * * @return \Magento\Backend\Model\View\Result\Redirect + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); @@ -86,7 +91,7 @@ public function execute() $role = $this->_initRole('role_id'); if (!$role->getId() && $rid) { - $this->messageManager->addError(__('This role no longer exists.')); + $this->messageManager->addErrorMessage(__('This role no longer exists.')); return $resultRedirect->setPath('adminhtml/*/'); } diff --git a/app/code/Magento/User/Controller/Adminhtml/User/Save.php b/app/code/Magento/User/Controller/Adminhtml/User/Save.php index d7d1c8b0e22a6..ebba96904aff5 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/Save.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/Save.php @@ -44,6 +44,9 @@ public function execute() { $userId = (int)$this->getRequest()->getParam('user_id'); $data = $this->getRequest()->getPostValue(); + if (array_key_exists('form_key', $data)) { + unset($data['form_key']); + } if (!$data) { $this->_redirect('adminhtml/*/'); return; diff --git a/app/code/Magento/User/composer.json b/app/code/Magento/User/composer.json index 431f33014d1a1..513a34639b0a5 100644 --- a/app/code/Magento/User/composer.json +++ b/app/code/Magento/User/composer.json @@ -12,7 +12,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-module", - "version": "101.0.5", + "version": "101.0.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Usps/composer.json b/app/code/Magento/Usps/composer.json index 8cbc56b96943a..93456fb9a0a94 100644 --- a/app/code/Magento/Usps/composer.json +++ b/app/code/Magento/Usps/composer.json @@ -15,7 +15,7 @@ "lib-libxml": "*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Webapi/composer.json b/app/code/Magento/Webapi/composer.json index 51c0302b5feab..d13034eddd5ac 100644 --- a/app/code/Magento/Webapi/composer.json +++ b/app/code/Magento/Webapi/composer.json @@ -14,7 +14,7 @@ "magento/module-customer": "101.0.*" }, "type": "magento2-module", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Weee/composer.json b/app/code/Magento/Weee/composer.json index cdcf68fb5422a..efb6bddb9289c 100644 --- a/app/code/Magento/Weee/composer.json +++ b/app/code/Magento/Weee/composer.json @@ -21,7 +21,7 @@ "magento/module-bundle": "100.2.*" }, "type": "magento2-module", - "version": "100.2.4", + "version": "100.2.5", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Weee/etc/adminhtml/system.xml b/app/code/Magento/Weee/etc/adminhtml/system.xml index ae02b27d10c72..d3e9efb8f0b46 100644 --- a/app/code/Magento/Weee/etc/adminhtml/system.xml +++ b/app/code/Magento/Weee/etc/adminhtml/system.xml @@ -44,6 +44,7 @@ <group id="totals_sort"> <field id="weee" translate="label" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Fixed Product Tax</label> + <validate>required-number validate-number</validate> </field> </group> </section> diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php index beac66dce5016..c1182505bc0c6 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php @@ -91,7 +91,7 @@ public function getMainFieldset() if ($this->_getData('main_fieldset') instanceof \Magento\Framework\Data\Form\Element\Fieldset) { return $this->_getData('main_fieldset'); } - $mainFieldsetHtmlId = 'options_fieldset' . md5($this->getWidgetType()); + $mainFieldsetHtmlId = 'options_fieldset' . hash('sha256', $this->getWidgetType()); $this->setMainFieldsetHtmlId($mainFieldsetHtmlId); $fieldset = $this->getForm()->addFieldset( $mainFieldsetHtmlId, @@ -141,7 +141,6 @@ protected function _addField($parameter) { $form = $this->getForm(); $fieldset = $this->getMainFieldset(); - //$form->getElement('options_fieldset'); // prepare element data with values (either from request of from default values) $fieldName = $parameter->getKey(); @@ -159,15 +158,19 @@ protected function _addField($parameter) $data['value'] = $parameter->getValue(); //prepare unique id value if ($fieldName == 'unique_id' && $data['value'] == '') { - $data['value'] = md5(microtime(1)); + $data['value'] = hash('sha256', microtime(1)); } } if (is_array($data['value'])) { foreach ($data['value'] as &$value) { - $value = html_entity_decode($value); + if (is_string($value)) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $value = html_entity_decode($value); + } } } else { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $data['value'] = html_entity_decode($data['value']); } diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/BuildWidget.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/BuildWidget.php index d9ef20aa90e47..b52df887b47e2 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/BuildWidget.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/BuildWidget.php @@ -6,6 +6,8 @@ */ namespace Magento\Widget\Controller\Adminhtml\Widget; +use Magento\Framework\App\ObjectManager; + class BuildWidget extends \Magento\Backend\App\Action { /** @@ -18,15 +20,25 @@ class BuildWidget extends \Magento\Backend\App\Action */ protected $_widget; + /** + * @var \Magento\Framework\Serialize\SerializerInterface + */ + private $serializer; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Widget\Model\Widget $widget + * @param \Magento\Framework\Serialize\SerializerInterface|null $serializer */ public function __construct( \Magento\Backend\App\Action\Context $context, - \Magento\Widget\Model\Widget $widget + \Magento\Widget\Model\Widget $widget, + \Magento\Framework\Serialize\SerializerInterface $serializer = null ) { $this->_widget = $widget; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get( + \Magento\Framework\Serialize\SerializerInterface::class + ); parent::__construct($context); } @@ -37,6 +49,13 @@ public function __construct( */ public function execute() { + if (!$this->getRequest()->isPost()) { + $this->getResponse()->representJson( + $this->serializer->serialize(['error' => true, 'message' => 'Invalid request']) + ); + return; + } + $type = $this->getRequest()->getPost('widget_type'); $params = $this->getRequest()->getPost('parameters', []); $asIs = $this->getRequest()->getPost('as_is'); diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Index.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Index.php index 6909f6058074e..c914f93e257f4 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Index.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Index.php @@ -6,6 +6,8 @@ */ namespace Magento\Widget\Controller\Adminhtml\Widget; +use Magento\Framework\Exception\NotFoundException; + class Index extends \Magento\Backend\App\Action { /** @@ -41,12 +43,16 @@ public function __construct( } /** - * Wisywyg widget plugin main page + * Wysiwyg widget plugin main page * * @return void + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found')); + } // save extra params for widgets insertion form $skipped = $this->getRequest()->getParam('skip_widgets'); $skipped = $this->_widgetConfig->decodeWidgetsFromQuery($skipped); diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Save.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Save.php index 98275c3b906db..0b38c5d99ca24 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Save.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Save.php @@ -16,7 +16,7 @@ class Save extends \Magento\Widget\Controller\Adminhtml\Widget\Instance public function execute() { $widgetInstance = $this->_initWidgetInstance(); - if (!$widgetInstance) { + if (!$this->getRequest()->isPost() || !$widgetInstance) { $this->_redirect('adminhtml/*/'); return; } diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml index 76ca2d1fed868..89812f98ef15e 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -32,6 +32,8 @@ <click selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="clickAddNewCondition"/> <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="{{widget.condition}}" stepKey="selectCondition"/> <waitForElement selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="waitRuleParameter"/> + <click selector="{{AdminNewWidgetSection.ruleParameterEdit}}" stepKey="clickRuleParameterEdit"/> + <selectOption selector="{{AdminNewWidgetSection.ruleParameterEditSelect}}" userInput="{{widget.ruleIsOneOf}}" stepKey="selectRuleParameterEdit"/> <click selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="clickRuleParameter"/> <click selector="{{AdminNewWidgetSection.openChooser}}" stepKey="clickChooser"/> <waitForPageLoad stepKey="waitForAjaxLoad"/> @@ -63,16 +65,34 @@ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been deleted" stepKey="seeSuccess"/> </actionGroup> + + <actionGroup name="AdminCreateWidgetWithBlockActionGroup" extends="AdminCreateWidgetActionGroup"> + <arguments> + <argument name="blockTitle" type="string"/> + </arguments> + <waitForElement selector="{{AdminNewWidgetSection.widgetSelectBlock}}" time="60" after="clickWidgetOptions" stepKey="waitForSelectBlock"/> + <click selector="{{AdminNewWidgetSection.widgetSelectBlock}}" stepKey="openSelectBlock"/> + <waitForPageLoad stepKey="waitForLoadBlocks"/> + <waitForElementVisible selector="{{AdminSelectWidgetBlockGridSection.sectionTitle}}" stepKey="waitSectionHeaderIsLoaded"/> + <selectOption selector="{{AdminSelectWidgetBlockGridSection.blockStatusFilter}}" userInput="Disabled" stepKey="chooseDisabledStatus"/> + <fillField selector="{{AdminSelectWidgetBlockGridSection.blockTitleFilter}}" userInput="{{blockTitle}}" stepKey="fillBlockTitle"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="searchBlock"/> + <click selector="{{AdminDataGridTableSection.row('1')}}" stepKey="clickSearchedBlock"/> + <waitForPageLoad stepKey="wait"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveWidget"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="AdminCreateProductLinkWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> <arguments> <argument name="product"/> </arguments> <selectOption selector="{{AdminNewWidgetSection.selectTemplate}}" userInput="{{widget.template}}" after="waitForPageLoad" stepKey="setTemplate"/> - <waitForAjaxLoad after="setTemplate" stepKey="waitForPageLoad2"/> + <waitForAjaxLoad stepKey="waitForPageLoad2"/> <click selector="{{AdminNewWidgetSection.selectProduct}}" after="clickWidgetOptions" stepKey="clickSelectProduct"/> - <fillField selector="{{AdminNewWidgetSelectProductPopupSection.filterBySku}}" userInput="{{product.sku}}" after="clickSelectProduct" stepKey="fillProductNameInFilter"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" after="fillProductNameInFilter" stepKey="applyFilter"/> - <click selector="{{AdminNewWidgetSelectProductPopupSection.firstRow}}" after="applyFilter" stepKey="selectProduct"/> + <fillField selector="{{AdminNewWidgetSelectProductPopupSection.filterBySku}}" userInput="{{product.sku}}" stepKey="fillProductNameInFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="applyFilter"/> + <click selector="{{AdminNewWidgetSelectProductPopupSection.firstRow}}" stepKey="selectProduct"/> <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> </actionGroup> diff --git a/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml index d7db8fe50cb7f..85f0d1170031c 100644 --- a/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml +++ b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml @@ -18,6 +18,7 @@ <data key="condition">SKU</data> <data key="display_on">All Pages</data> <data key="container">Main Content Area</data> + <data key="ruleIsOneOf">is one of</data> </entity> <entity name="DynamicBlocksRotatorWidget" type="widget"> <data key="type">Banner Rotator</data> @@ -32,4 +33,14 @@ <data key="display_mode">salesrule</data> <data key="restrict_type">header</data> </entity> + <entity name="WidgetWithBlock" type="widget"> + <data key="type">CMS Static Block</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">testName</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="display_on">All Pages</data> + <data key="container">Page Top</data> + </entity> </entities> diff --git a/app/code/Magento/Widget/Test/Mftf/Page/AdminNewWidgetPage.xml b/app/code/Magento/Widget/Test/Mftf/Page/AdminNewWidgetPage.xml index d495a36f68d0a..da77738c7d013 100644 --- a/app/code/Magento/Widget/Test/Mftf/Page/AdminNewWidgetPage.xml +++ b/app/code/Magento/Widget/Test/Mftf/Page/AdminNewWidgetPage.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="AdminNewWidgetPage" url="admin/widget_instance/new/" area="admin" module="Magento_Widget"> <section name="AdminNewWidgetSection"/> + <section name="AdminSelectWidgetBlockGridSection"/> </page> </pages> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index 472539bdcc7ed..fe46a3d66bfad 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -21,6 +21,8 @@ <element name="widgetOptions" type="select" selector="#widget_instace_tabs_properties_section"/> <element name="addNewCondition" type="select" selector=".rule-param.rule-param-new-child"/> <element name="selectCondition" type="input" selector="#conditions__1__new_child"/> + <element name="ruleParameterEdit" type="select" selector="#conditions__1__children>li:nth-child(1)>span:nth-child(3)>a"/> + <element name="ruleParameterEditSelect" type="select" selector="#conditions__1__children>li:nth-child(1)>span:nth-child(3)>span>select"/> <element name="ruleParameter" type="select" selector="#conditions__1__children>li:nth-child(1)>span:nth-child(4)>a"/> <element name="setRuleParameter" type="input" selector="#conditions__1--1__value"/> <element name="applyParameter" type="button" selector=".rule-param-apply"/> @@ -34,5 +36,6 @@ <element name="conditionOperatorSelect" type="select" selector="#conditions__1--{{arg1}}__operator" parameterized="true"/> <element name="displayMode" type="select" selector="select[id*='display_mode']"/> <element name="restrictTypes" type="select" selector="select[id*='types']"/> + <element name="widgetSelectBlock" type="button" selector=".action-default.scalable.btn-chooser"/> </section> </sections> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminSelectWidgetBlockGridSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminSelectWidgetBlockGridSection.xml new file mode 100644 index 0000000000000..d46d6edf8026b --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminSelectWidgetBlockGridSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminSelectWidgetBlockGridSection"> + <element name="blockTitleFilter" type="input" selector=".modal-content input[name='chooser_title']"/> + <element name="blockStatusFilter" type="select" selector=".modal-content select[name='chooser_is_active']"/> + <element name="sectionTitle" type="button" selector="//*[@class='modal-header']//h1[contains(text(), 'Select Block')]"/> + </section> +</sections> diff --git a/app/code/Magento/Widget/composer.json b/app/code/Magento/Widget/composer.json index eb3e0af2b2bc6..fc63ae00bdb7a 100644 --- a/app/code/Magento/Widget/composer.json +++ b/app/code/Magento/Widget/composer.json @@ -16,7 +16,7 @@ "magento/module-widget-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.5", + "version": "101.0.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Wishlist/Block/Customer/Sharing.php b/app/code/Magento/Wishlist/Block/Customer/Sharing.php index 6fbf5a23dca22..992946363186c 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Sharing.php +++ b/app/code/Magento/Wishlist/Block/Customer/Sharing.php @@ -11,6 +11,8 @@ */ namespace Magento\Wishlist\Block\Customer; +use Magento\Captcha\Block\Captcha; + /** * @api * @since 100.0.2 @@ -60,6 +62,20 @@ public function __construct( */ protected function _prepareLayout() { + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => 'share_wishlist_form', + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + $this->pageConfig->getTitle()->set(__('Wish List Sharing')); } diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php index cafb6a5291481..40882ae00dae1 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php @@ -5,14 +5,15 @@ */ /** - * Wishlist for item column in customer wishlist - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Model for item column in customer wishlist. + * * @api + * @deprecated * @since 100.0.2 */ class Actions extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php index 1ba31b26df46e..97cfd91b32498 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php @@ -6,8 +6,6 @@ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; -use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; - /** * Wishlist block customer item cart column * @@ -16,6 +14,28 @@ */ class Cart extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column { + /** + * @var View + */ + private $productView; + + /** + * @param \Magento\Catalog\Block\Product\Context $context + * @param \Magento\Framework\App\Http\Context $httpContext + * @param \Magento\Catalog\Block\Product\View $productView + * @param array $data + */ + public function __construct( + \Magento\Catalog\Block\Product\Context $context, + \Magento\Framework\App\Http\Context $httpContext, + array $data = [], + \Magento\Catalog\Block\Product\View $productView = null + ) { + $this->productView = $productView ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Catalog\Block\Product\View::class); + parent::__construct($context, $httpContext, $data); + } + /** * Returns qty to show visually to user * @@ -25,7 +45,9 @@ class Cart extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column public function getAddToCartQty(\Magento\Wishlist\Model\Item $item) { $qty = $item->getQty(); - return $qty ? $qty : 1; + $qty = $qty < $this->productView->getProductDefaultQty($this->getProductItem()) + ? $this->productView->getProductDefaultQty($this->getProductItem()) : $qty; + return $qty ?: 1; } /** @@ -37,28 +59,4 @@ public function getProductItem() { return $this->getItem()->getProduct(); } - - /** - * Get min and max qty for wishlist form. - * - * @return array - */ - public function getMinMaxQty(): array - { - $stockItem = $this->stockRegistry->getStockItem( - $this->getItem()->getProduct()->getId(), - $this->getItem()->getProduct()->getStore()->getWebsiteId() - ); - - $params = []; - - $params['minAllowed'] = (float)$stockItem->getMinSaleQty(); - if ($stockItem->getMaxSaleQty()) { - $params['maxAllowed'] = (float)$stockItem->getMaxSaleQty(); - } else { - $params['maxAllowed'] = (float)StockDataFilter::MAX_QTY_VALUE; - } - - return $params; - } } diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php index 2d75956858a0a..53f67626e956d 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php @@ -5,14 +5,15 @@ */ /** - * Wishlist block customer item cart column - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Wishlist block customer item cart column. + * * @api + * @deprecated * @since 100.0.2 */ class Comment extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php index 53ca78c63524d..c4c786961694b 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php @@ -5,14 +5,15 @@ */ /** - * Edit item in customer wishlist table - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Edit item in customer wishlist table. + * * @api + * @deprecated * @since 100.0.2 */ class Edit extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php index 33fb0f7325cdd..b7eaf53fc23b5 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php @@ -5,14 +5,15 @@ */ /** - * Wishlist block customer item cart column - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Wishlist block customer item cart column. + * * @api + * @deprecated * @since 100.0.2 */ class Info extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php index 57703b9300db8..09f5014edead6 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php @@ -5,14 +5,15 @@ */ /** - * Delete item column in customer wishlist table - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Delete item column in customer wishlist table + * * @api + * @deprecated * @since 100.0.2 */ class Remove extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Controller/Index/Send.php b/app/code/Magento/Wishlist/Controller/Index/Send.php index c2389af6a2282..7ca94b1f284f5 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Send.php +++ b/app/code/Magento/Wishlist/Controller/Index/Send.php @@ -8,11 +8,20 @@ use Magento\Framework\App\Action; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Session\Generic as WishlistSession; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\View\Result\Layout as ResultLayout; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Captcha\Model\DefaultModel as CaptchaModel; +use Magento\Framework\Exception\LocalizedException; +use Magento\Customer\Model\Customer; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -69,6 +78,16 @@ class Send extends \Magento\Wishlist\Controller\AbstractIndex */ protected $storeManager; + /** + * @var CaptchaHelper + */ + private $captchaHelper; + + /** + * @var CaptchaStringResolver + */ + private $captchaStringResolver; + /** * @param Action\Context $context * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator @@ -81,6 +100,8 @@ class Send extends \Magento\Wishlist\Controller\AbstractIndex * @param WishlistSession $wishlistSession * @param ScopeConfigInterface $scopeConfig * @param StoreManagerInterface $storeManager + * @param CaptchaHelper|null $captchaHelper + * @param CaptchaStringResolver|null $captchaStringResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -94,7 +115,9 @@ public function __construct( \Magento\Customer\Helper\View $customerHelperView, WishlistSession $wishlistSession, ScopeConfigInterface $scopeConfig, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + CaptchaHelper $captchaHelper = null, + CaptchaStringResolver $captchaStringResolver = null ) { $this->_formKeyValidator = $formKeyValidator; $this->_customerSession = $customerSession; @@ -106,27 +129,45 @@ public function __construct( $this->wishlistSession = $wishlistSession; $this->scopeConfig = $scopeConfig; $this->storeManager = $storeManager; + $this->captchaHelper = $captchaHelper ?: ObjectManager::getInstance()->get(CaptchaHelper::class); + $this->captchaStringResolver = $captchaStringResolver ? + : ObjectManager::getInstance()->get(CaptchaStringResolver::class); + parent::__construct($context); } /** - * Share wishlist - * - * @return \Magento\Framework\Controller\Result\Redirect + * @return ResponseInterface|Redirect|ResultInterface * @throws NotFoundException + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Zend_Validate_Exception */ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + $captchaFormName = 'share_wishlist_form'; + /** @var CaptchaModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha($captchaFormName); + if (!$this->_formKeyValidator->validate($this->getRequest())) { $resultRedirect->setPath('*/*/'); return $resultRedirect; } + $isCorrectCaptcha = $this->validateCaptcha($captchaModel, $captchaFormName); + + $this->logCaptchaAttempt($captchaModel); + + if (!$isCorrectCaptcha) { + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); + $resultRedirect->setPath('*/*/share'); + return $resultRedirect; + } + $wishlist = $this->wishlistProvider->getWishlist(); if (!$wishlist) { throw new NotFoundException(__('Page not found.')); @@ -288,4 +329,43 @@ protected function getWishlistItems(ResultLayout $resultLayout) ->getBlock('wishlist.email.items') ->toHtml(); } + + /** + * Log customer action attempts + * @param CaptchaModel $captchaModel + * @return void + */ + private function logCaptchaAttempt(CaptchaModel $captchaModel) + { + /** @var Customer $customer */ + $customer = $this->_customerSession->getCustomer(); + $email = ''; + + if ($customer->getId()) { + $email = $customer->getEmail(); + } + + $captchaModel->logAttempt($email); + } + + /** + * @param CaptchaModel $captchaModel + * @param string $captchaFormName + * @return bool + */ + private function validateCaptcha(CaptchaModel $captchaModel, string $captchaFormName) : bool + { + if ($captchaModel->isRequired()) { + $word = $this->captchaStringResolver->resolve( + $this->getRequest(), + $captchaFormName + ); + + if (!$captchaModel->isCorrect($word)) { + return false; + } + } + + return true; + } } diff --git a/app/code/Magento/Wishlist/Model/Rss/Wishlist.php b/app/code/Magento/Wishlist/Model/Rss/Wishlist.php index 75df3027ad9a9..b73cae240b369 100644 --- a/app/code/Magento/Wishlist/Model/Rss/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Rss/Wishlist.php @@ -7,6 +7,7 @@ namespace Magento\Wishlist\Model\Rss; use Magento\Framework\App\Rss\DataProviderInterface; +use Magento\Store\Model\ScopeInterface; /** * Wishlist RSS model @@ -114,10 +115,8 @@ public function __construct( */ public function isAllowed() { - return (bool)$this->scopeConfig->getValue( - 'rss/wishlist/active', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + return $this->scopeConfig->isSetFlag('rss/wishlist/active', ScopeInterface::SCOPE_STORE) + && $this->getWishlist()->getCustomerId() == $this->wishlistHelper->getCustomer()->getId(); } /** @@ -180,8 +179,8 @@ public function getRssData() } } else { $data = [ - 'title' => __('We cannot retrieve the Wish List.'), - 'description' => __('We cannot retrieve the Wish List.'), + 'title' => __('We cannot retrieve the Wish List.')->render(), + 'description' => __('We cannot retrieve the Wish List.')->render(), 'link' => $this->urlBuilder->getUrl(), 'charset' => 'UTF-8', ]; @@ -195,7 +194,7 @@ public function getRssData() */ public function getCacheKey() { - return 'rss_wishlist_data'; + return 'rss_wishlist_data_' . $this->getWishlist()->getId(); } /** @@ -215,7 +214,7 @@ public function getHeader() { $customerId = $this->getWishlist()->getCustomerId(); $customer = $this->customerFactory->create()->load($customerId); - $title = __('%1\'s Wishlist', $customer->getName()); + $title = __('%1\'s Wishlist', $customer->getName())->render(); $newUrl = $this->urlBuilder->getUrl( 'wishlist/shared/index', ['code' => $this->getWishlist()->getSharingCode()] diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml index 920b387441ada..7a88d6db79200 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml @@ -88,25 +88,15 @@ <see selector="{{StorefrontMessagesSection.success}}" userInput="{{product.name}} has been updated in your Wish List." stepKey="successMessage"/> </actionGroup> - <actionGroup name="StorefrontCustomerEditProductInWishlistMakeQuantityValidationError"> + <actionGroup name="StorefrontValidateQtyAfterEditProductInWishlist" extends="StorefrontCustomerEditProductInWishlist"> <arguments> - <argument name="product"/> - <argument name="description" type="string"/> - <argument name="quantity" type="string"/> - <argument name="errorNum" type="string"/> + <argument name="maxQtyAllowed" type="string" default="10000"/> </arguments> - <scrollToTopOfPage stepKey="scrollToTop1"/> - <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productInfoByName(product.name)}}" stepKey="mouseOverOnProduct" /> - <fillField selector="{{StorefrontCustomerWishlistProductSection.productDescription(product.name)}}" userInput="{{description}}" stepKey="fillDescription"/> - <fillField selector="{{StorefrontCustomerWishlistProductSection.productQuantity(product.name)}}" userInput="{{quantity}}" stepKey="fillQuantity"/> - <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productAddAllToCart}}" stepKey="mouseOver1"/> - <click selector="{{StorefrontCustomerWishlistProductSection.productUpdateWishList}}" stepKey="clickAddToWishlistButton"/> - <waitForElement selector="{{StorefrontCustomerWishlistProductSection.productQtyError(product.name)}}" stepKey="waitForErrorMessage"/> - <scrollToTopOfPage stepKey="scrollToTop2"/> - - <!--Check error message--> - <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productInfoByName(product.name)}}" stepKey="wishlistMoveMouseOverProduct" /> - <see selector="{{StorefrontCustomerWishlistProductSection.productQtyError(product.name)}}" userInput="The maximum you may purchase is {{errorNum}}." stepKey="checkQtyError"/> - <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productAddAllToCart}}" stepKey="mouseOver2"/> + <remove keyForRemoval="successMessage"/> + <waitForAjaxLoad after="submitUpdateWishlist" stepKey="waitForAjaxLoad"/> + <scrollToTopOfPage after="waitForAjaxLoad" stepKey="scrollToTop"/> + <moveMouseOver selector="{{StorefrontCustomerWishlistProductSection.productInfoByName(product.name)}}" after="scrollToTop" stepKey="moveMouseOverProduct"/> + <waitForElementVisible selector="{{StorefrontCustomerWishlistProductSection.productQtyError(product.name)}}" after="moveMouseOverProduct" stepKey="waitForErrorMessage"/> + <see selector="{{StorefrontCustomerWishlistProductSection.productQtyError(product.name)}}" userInput="The maximum you may purchase is {{maxQtyAllowed}}." after="waitForErrorMessage" stepKey="seeErrorMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Data/ProductStockOptionsData.xml b/app/code/Magento/Wishlist/Test/Mftf/Data/ProductStockOptionsData.xml new file mode 100644 index 0000000000000..07a51e444a7e4 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Data/ProductStockOptionsData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultMaxQtyAllowedConfig" type="max_qty_allowed_config"> + <requiredEntity type="max_qty_allowed_config_default">DefaultMaxQtyAllowed</requiredEntity> + </entity> + <entity name="DefaultMaxQtyAllowed" type="max_qty_allowed_config_default"> + <data key="value">0</data> + </entity> + + <entity name="SetMaxQtyAllowedConfigZero" type="max_qty_allowed_config"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Metadata/product_stock_options-meta.xml b/app/code/Magento/Wishlist/Test/Mftf/Metadata/product_stock_options-meta.xml new file mode 100644 index 0000000000000..748144ca6dc9f --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Metadata/product_stock_options-meta.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="MaxQtyAllowedConfig" dataType="max_qty_allowed_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/cataloginventory/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="max_qty_allowed_config"> + <object key="item_options" dataType="max_qty_allowed_config"> + <object key="fields" dataType="max_qty_allowed_config"> + <object key="max_sale_qty" dataType="max_qty_allowed_config"> + <field key="value">string</field> + <object key="inherit" dataType="max_qty_allowed_config_default"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml index ea9c4748f67b1..dd8814b199e13 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistProductSection.xml @@ -17,9 +17,8 @@ <element name="productImageByImageName" type="text" selector="//main//li//a//img[contains(@src, '{{var1}}')]" parameterized="true"/> <element name="productDescription" type="input" selector="//a[contains(text(), '{{productName}}')]/ancestor::div[@class='product-item-info']//textarea[@class='product-item-comment']" parameterized="true"/> <element name="productQuantity" type="input" selector="//a[contains(text(), '{{productName}}')]/ancestor::div[@class='product-item-info']//input[@class='input-text qty']" parameterized="true"/> - <element name="productEditButtonByName" type="button" selector="//li[.//a[contains(text(), '{{var1}}')]]//span[contains(text(), 'Edit')]" parameterized="true"/> <element name="productUpdateWishList" type="button" selector=".column.main .actions-toolbar .action.update" timeout="30"/> <element name="productAddAllToCart" type="button" selector=".column.main .actions-toolbar .action.tocart" timeout="30"/> - <element name="productQtyError" type="text" selector="//li[.//a[contains(text(), '{{var1}}')]]//div[@class='mage-error']" parameterized="true" timeout="30"/> + <element name="productQtyError" type="text" selector="//li[.//a[contains(text(), '{{productName}}')]]//div[@class='mage-error']" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistSection.xml b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistSection.xml index 5d40d9a38d656..bfacc8093bede 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistSection.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistSection.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerWishlistSection"> <element name="productItemNameText" type="text" selector=".products-grid .product-item-name a"/> - <element name="removeWishlistButton" type="button" selector=".products-grid .btn-remove.action.delete>span" timeout="30"/> + <element name="removeWishlistButton" type="button" selector=".products-grid .btn-remove.action.delete" timeout="30"/> <element name="emptyWishlistText" type="text" selector=".message.info.empty>span"/> <element name="successMsg" type="text" selector="div.message-success.success.message"/> </section> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckAmountLimitWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckAmountLimitWishlistTest.xml deleted file mode 100644 index 3bc924a7e60fa..0000000000000 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckAmountLimitWishlistTest.xml +++ /dev/null @@ -1,60 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontCheckAmountLimitWishlistTest"> - <annotations> - <stories value="MAGETWO-73613: My Wishlist - quantity input box issue"/> - <title value="Check amount limit for Wishlist"/> - <description value="Check amount limit for Wishlist with different config settings"/> - <features value="Wishlist"/> - <severity value="AVERAGE"/> - <testCaseId value="MAGETWO-96606"/> - <group value="wishlist"/> - </annotations> - <before> - <createData entity="SimpleSubCategory" stepKey="createCategory"/> - <createData entity="SimpleProduct" stepKey="createProduct"> - <requiredEntity createDataKey="createCategory"/> - </createData> - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - </before> - <after> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="logoutCustomerAccount"/> - <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <createData entity="DefaultProductStockOptions" stepKey="changeToDefaultQtyAllowAmount"/> - </after> - <!--Login as Customer--> - <actionGroup ref="CustomerLoginOnStorefront" stepKey="loginToStorefrontAccount"> - <argument name="customer" value="$$createCustomer$$"/> - </actionGroup> - <!--Go to category page--> - <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCreateCategoryPage"/> - <!--Add created product to Wish List--> - <actionGroup ref="StorefrontCustomerAddCategoryProductToWishlistActionGroup" stepKey="addSimpleProduct1ToWishlist"> - <argument name="productVar" value="$$createProduct$$"/> - </actionGroup> - <actionGroup ref="StorefrontCustomerEditProductInWishlistMakeQuantityValidationError" stepKey="checkWishListError1"> - <argument name="product" value="$$createProduct$$"/> - <argument name="description" value="It`s my dream"/> - <argument name="quantity" value="1234567890"/> - <argument name="errorNum" value="10000"/> - </actionGroup> - <createData entity="ProductStockOptions" stepKey="changeDefaultQtyAllowAmount"/> - <!--Go to wishlist page--> - <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="amOnWishlist" /> - <actionGroup ref="StorefrontCustomerEditProductInWishlistMakeQuantityValidationError" stepKey="checkWishListError2"> - <argument name="product" value="$$createProduct$$"/> - <argument name="description" value="It`s my dream"/> - <argument name="quantity" value="1234567890"/> - <argument name="errorNum" value="99999999"/> - </actionGroup> - </test> -</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckingMaxQtyAllowedInWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckingMaxQtyAllowedInWishlistTest.xml new file mode 100644 index 0000000000000..4e9d3d9f1729b --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckingMaxQtyAllowedInWishlistTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckingMaxQtyAllowedInWishlistTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Update wishlist item"/> + <title value="Validate quantity field during updating wishlist item"/> + <description value="Validate quantity field during updating wishlist item"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-7192"/> + <useCaseId value="MAGETWO-73613"/> + <group value="wishlist"/> + </annotations> + <before> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create product--> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Login as Customer--> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$createCustomer$" /> + </actionGroup> + </before> + <after> + <!--Delete entities--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Set "Maximum Qty Allowed in Shopping Cart" config to default--> + <createData entity="DefaultMaxQtyAllowedConfig" stepKey="setMaxQtyAllowedConfigToDefault"/> + <!--Logout--> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogout"/> + </after> + + <!--Go to product page--> + <amOnPage url="{{StorefrontProductPage.url($createProduct.custom_attributes[url_key]$)}}" stepKey="goToProductPage"/> + <!--Add product to Wishlist--> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductToWishlist"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <!--Validate quantity after edit product in Wishlist - Case 1--> + <actionGroup ref="StorefrontValidateQtyAfterEditProductInWishlist" stepKey="validateQtyFirstCase"> + <argument name="product" value="$createProduct$"/> + <argument name="description" value="Test Description"/> + <argument name="quantity" value="222222222222222"/> + <argument name="maxQtyAllowed" value="10000"/> + </actionGroup> + <!--Set "Maximum Qty Allowed in Shopping Cart" config to "0"--> + <createData entity="SetMaxQtyAllowedConfigZero" stepKey="setMaxQtyAllowedConfigToZero"/> + <!--Go to Wishlist page--> + <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="goToWishlistPage"/> + <!--Validate quantity after edit product in Wishlist - Case 1--> + <actionGroup ref="StorefrontValidateQtyAfterEditProductInWishlist" stepKey="validateQtySecondCase"> + <argument name="product" value="$createProduct$"/> + <argument name="description" value="Test Description"/> + <argument name="quantity" value="222222222222222"/> + <argument name="maxQtyAllowed" value="99999999"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml index 66c636871c39a..2f17c29a6556b 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml @@ -6,14 +6,16 @@ */ --> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontDeletePersistedWishlistTest"> <annotations> - <features value="Delete a persist wishlist for a customer"/> + <features value="Wishlist"/> <stories value="Delete a persist wishlist for a customer"/> - <title value="Delete a persist wishlist for a customer"/> - <description value="Delete a persist wishlist for a customer"/> + <title value="Customer should be able to delete a persistent wishlist"/> + <description value="Customer should be able to delete a persistent wishlist"/> + <severity value="AVERAGE"/> <group value="wishlist"/> + <testCaseId value="MC-4110"/> </annotations> <before> <createData stepKey="category" entity="SimpleSubCategory"/> diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php index a8c0fbb951cce..34ad89e208b5f 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php @@ -5,32 +5,25 @@ */ namespace Magento\Wishlist\Test\Unit\Controller\Index; -use Magento\Customer\Helper\View as CustomerViewHelper; +use Magento\Customer\CustomerData\Customer; use Magento\Customer\Model\Data\Customer as CustomerData; -use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\App\Action\Context as ActionContext; -use Magento\Framework\App\Area; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\Controller\Result\Redirect as ResultRedirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Event\ManagerInterface as EventManagerInterface; -use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Mail\TransportInterface; use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\Session\Generic as WishlistSession; -use Magento\Framework\Translate\Inline\StateInterface as TranslateInlineStateInterface; use Magento\Framework\UrlInterface; -use Magento\Framework\View\Layout; use Magento\Framework\View\Result\Layout as ResultLayout; -use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; use Magento\Wishlist\Controller\Index\Send; use Magento\Wishlist\Controller\WishlistProviderInterface; -use Magento\Wishlist\Model\Config as WishlistConfig; -use Magento\Wishlist\Model\Wishlist; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Model\DefaultModel as CaptchaModel; +use Magento\Customer\Model\Session; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -47,36 +40,12 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var FormKeyValidator |\PHPUnit_Framework_MockObject_MockObject */ protected $formKeyValidator; - /** @var CustomerSession |\PHPUnit_Framework_MockObject_MockObject */ - protected $customerSession; - /** @var WishlistProviderInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $wishlistProvider; - /** @var WishlistConfig |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlistConfig; - - /** @var TransportBuilder |\PHPUnit_Framework_MockObject_MockObject */ - protected $transportBuilder; - - /** @var TranslateInlineStateInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $inlineTranslation; - - /** @var CustomerViewHelper |\PHPUnit_Framework_MockObject_MockObject */ - protected $customerViewHelper; - - /** @var WishlistSession |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlistSession; - - /** @var ScopeConfigInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $scopeConfig; - /** @var Store |\PHPUnit_Framework_MockObject_MockObject */ protected $store; - /** @var StoreManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; - /** @var ResultFactory |\PHPUnit_Framework_MockObject_MockObject */ protected $resultFactory; @@ -86,15 +55,9 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var ResultLayout |\PHPUnit_Framework_MockObject_MockObject */ protected $resultLayout; - /** @var Layout |\PHPUnit_Framework_MockObject_MockObject */ - protected $layout; - /** @var RequestInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $request; - /** @var Wishlist |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlist; - /** @var ManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $messageManager; @@ -110,6 +73,14 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var EventManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $eventManager; + /** @var CaptchaHelper |\PHPUnit_Framework_MockObject_MockObject */ + protected $captchaHelper; + + /** @var CaptchaModel |\PHPUnit_Framework_MockObject_MockObject */ + protected $captchaModel; + /** @var Session |\PHPUnit_Framework_MockObject_MockObject */ + protected $customerSession; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -136,7 +107,7 @@ protected function setUp() $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->setMethods([ 'getPost', - 'getPostValue', + 'getPostValue' ]) ->getMockForAbstractClass(); @@ -172,90 +143,72 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->wishlistProvider = $this->getMockBuilder(\Magento\Wishlist\Controller\WishlistProviderInterface::class) - ->getMockForAbstractClass(); - - $this->wishlistConfig = $this->getMockBuilder(\Magento\Wishlist\Model\Config::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->transportBuilder = $this->getMockBuilder(\Magento\Framework\Mail\Template\TransportBuilder::class) + $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) ->disableOriginalConstructor() + ->setMethods([ + 'getEmail', + 'getId' + ]) ->getMock(); - $this->inlineTranslation = $this->getMockBuilder(\Magento\Framework\Translate\Inline\StateInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $customerMock->expects($this->any()) + ->method('getEmail') + ->willReturn('expamle@mail.com'); - $this->customerViewHelper = $this->getMockBuilder(\Magento\Customer\Helper\View::class) - ->disableOriginalConstructor() - ->getMock(); + $customerMock->expects($this->any()) + ->method('getId') + ->willReturn(false); - $this->wishlistSession = $this->getMockBuilder(\Magento\Framework\Session\Generic::class) + $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) ->disableOriginalConstructor() - ->setMethods(['setSharingForm']) + ->setMethods([ + 'getCustomer', + 'getData' + ]) ->getMock(); - $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $this->customerSession->expects($this->any()) + ->method('getCustomer') + ->willReturn($customerMock); - $this->store = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->setMethods(['getStoreId']) - ->getMock(); + $this->customerSession->expects($this->any()) + ->method('getData') + ->willReturn(false); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); + $this->wishlistProvider = $this->getMockBuilder(\Magento\Wishlist\Controller\WishlistProviderInterface::class) + ->getMockForAbstractClass(); - $this->wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + $this->captchaHelper = $this->getMockBuilder(CaptchaHelper::class) ->disableOriginalConstructor() ->setMethods([ - 'getShared', - 'setShared', - 'getId', - 'getSharingCode', - 'save', - 'isSalable', + 'getCaptcha' ]) ->getMock(); - $this->customerData = $this->getMockBuilder(\Magento\Customer\Model\Data\Customer::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->layout = $this->getMockBuilder(\Magento\Framework\View\Layout::class) + $this->captchaModel = $this->getMockBuilder(CaptchaModel::class) ->disableOriginalConstructor() ->setMethods([ - 'getBlock', - 'setWishlistId', - 'toHtml', + 'isRequired', + 'logAttempt' ]) ->getMock(); - $this->transport = $this->getMockBuilder(\Magento\Framework\Mail\TransportInterface::class) - ->getMockForAbstractClass(); + $objectHelper = new ObjectManager($this); - $this->model = new Send( - $this->context, - $this->formKeyValidator, - $this->customerSession, - $this->wishlistProvider, - $this->wishlistConfig, - $this->transportBuilder, - $this->inlineTranslation, - $this->customerViewHelper, - $this->wishlistSession, - $this->scopeConfig, - $this->storeManager + $this->captchaHelper->expects($this->once())->method('getCaptcha') + ->willReturn($this->captchaModel); + $this->captchaModel->expects($this->any())->method('isRequired') + ->willReturn(false); + + $this->model = $objectHelper->getObject( + Send::class, + [ + 'context' => $this->context, + 'formKeyValidator' => $this->formKeyValidator, + 'wishlistProvider' => $this->wishlistProvider, + 'captchaHelper' => $this->captchaHelper, + '_customerSession' => $this->customerSession + ] ); } @@ -291,409 +244,4 @@ public function testExecuteNoWishlistAvailable() $this->model->execute(); } - - /** - * @param string $text - * @param int $textLimit - * @param string $emails - * @param int $emailsLimit - * @param int $shared - * @param string $postValue - * @param string $errorMessage - * - * @dataProvider dataProviderExecuteWithError - */ - public function testExecuteWithError( - $text, - $textLimit, - $emails, - $emailsLimit, - $shared, - $postValue, - $errorMessage - ) { - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->once()) - ->method('getShared') - ->willReturn($shared); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->once()) - ->method('getPostValue') - ->willReturn($postValue); - - $this->messageManager->expects($this->once()) - ->method('addError') - ->with($errorMessage) - ->willReturnSelf(); - - $this->wishlistSession->expects($this->any()) - ->method('setSharingForm') - ->with($postValue) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*/share') - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } - - /** - * 1. Text - * 2. Text limit - * 3. Emails - * 4. Emails limit - * 5. Shared wishlists counter - * 6. POST value - * 7. Error message (RESULT) - * - * @return array - */ - public function dataProviderExecuteWithError() - { - return [ - ['test text', 1, 'user1@example.com', 1, 0, '', 'Message length must not exceed 1 symbols'], - ['test text', 100, null, 1, 0, '', 'Please enter an email address.'], - ['test text', 100, '', 1, 0, '', 'Please enter an email address.'], - ['test text', 100, 'user1@example.com', 1, 1, '', 'This wish list can be shared 0 more times.'], - [ - 'test text', - 100, - 'u1@example.com, u2@example.com', - 3, - 2, - '', - 'This wish list can be shared 1 more times.' - ], - ['test text', 100, 'wrongEmailAddress', 1, 0, '', 'Please enter a valid email address.'], - ['test text', 100, 'user1@example.com, wrongEmailAddress', 2, 0, '', 'Please enter a valid email address.'], - ['test text', 100, 'wrongEmailAddress, user2@example.com', 2, 0, '', 'Please enter a valid email address.'], - ]; - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithException() - { - $text = 'test text'; - $textLimit = 100; - $emails = 'user1@example.com'; - $emailsLimit = 1; - $shared = 0; - $customerName = 'user1 user1'; - $wishlistId = 1; - $rssLink = 'rss link'; - $sharingCode = 'sharing code'; - $exceptionMessage = 'test exception message'; - $postValue = ''; - - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->exactly(2)) - ->method('getShared') - ->willReturn($shared); - $this->wishlist->expects($this->once()) - ->method('setShared') - ->with($shared) - ->willReturnSelf(); - $this->wishlist->expects($this->once()) - ->method('getId') - ->willReturn($wishlistId); - $this->wishlist->expects($this->once()) - ->method('getSharingCode') - ->willReturn($sharingCode); - $this->wishlist->expects($this->once()) - ->method('save') - ->willReturnSelf(); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->exactly(2)) - ->method('getParam') - ->with('rss_url') - ->willReturn(true); - $this->request->expects($this->once()) - ->method('getPostValue') - ->willReturn($postValue); - - $this->layout->expects($this->once()) - ->method('getBlock') - ->with('wishlist.email.rss') - ->willReturnSelf(); - $this->layout->expects($this->once()) - ->method('setWishlistId') - ->with($wishlistId) - ->willReturnSelf(); - $this->layout->expects($this->once()) - ->method('toHtml') - ->willReturn($rssLink); - - $this->resultLayout->expects($this->exactly(2)) - ->method('addHandle') - ->willReturnMap([ - ['wishlist_email_rss', null], - ['wishlist_email_items', null], - ]); - $this->resultLayout->expects($this->once()) - ->method('getLayout') - ->willReturn($this->layout); - - $this->inlineTranslation->expects($this->once()) - ->method('suspend') - ->willReturnSelf(); - $this->inlineTranslation->expects($this->once()) - ->method('resume') - ->willReturnSelf(); - - $this->customerSession->expects($this->once()) - ->method('getCustomerDataObject') - ->willReturn($this->customerData); - - $this->customerViewHelper->expects($this->once()) - ->method('getCustomerName') - ->with($this->customerData) - ->willReturn($customerName); - - // Throw Exception - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->willThrowException(new \Exception($exceptionMessage)); - - $this->messageManager->expects($this->once()) - ->method('addError') - ->with($exceptionMessage) - ->willReturnSelf(); - - $this->wishlistSession->expects($this->any()) - ->method('setSharingForm') - ->with($postValue) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*/share') - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $text = 'text'; - $textLimit = 100; - $emails = 'user1@example.com'; - $emailsLimit = 1; - $shared = 0; - $customerName = 'user1 user1'; - $wishlistId = 1; - $sharingCode = 'sharing code'; - $templateIdentifier = 'template identifier'; - $storeId = 1; - $viewOnSiteLink = 'view on site link'; - $from = 'user0@example.com'; - - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->exactly(2)) - ->method('getShared') - ->willReturn($shared); - $this->wishlist->expects($this->once()) - ->method('setShared') - ->with(++$shared) - ->willReturnSelf(); - $this->wishlist->expects($this->exactly(2)) - ->method('getId') - ->willReturn($wishlistId); - $this->wishlist->expects($this->once()) - ->method('getSharingCode') - ->willReturn($sharingCode); - $this->wishlist->expects($this->once()) - ->method('save') - ->willReturnSelf(); - $this->wishlist->expects($this->once()) - ->method('isSalable') - ->willReturn(true); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->exactly(2)) - ->method('getParam') - ->with('rss_url') - ->willReturn(true); - - $this->layout->expects($this->exactly(2)) - ->method('getBlock') - ->willReturnMap([ - ['wishlist.email.rss', $this->layout], - ['wishlist.email.items', $this->layout], - ]); - - $this->layout->expects($this->once()) - ->method('setWishlistId') - ->with($wishlistId) - ->willReturnSelf(); - $this->layout->expects($this->exactly(2)) - ->method('toHtml') - ->willReturn($text); - - $this->resultLayout->expects($this->exactly(2)) - ->method('addHandle') - ->willReturnMap([ - ['wishlist_email_rss', null], - ['wishlist_email_items', null], - ]); - $this->resultLayout->expects($this->exactly(2)) - ->method('getLayout') - ->willReturn($this->layout); - - $this->inlineTranslation->expects($this->once()) - ->method('suspend') - ->willReturnSelf(); - $this->inlineTranslation->expects($this->once()) - ->method('resume') - ->willReturnSelf(); - - $this->customerSession->expects($this->once()) - ->method('getCustomerDataObject') - ->willReturn($this->customerData); - - $this->customerViewHelper->expects($this->once()) - ->method('getCustomerName') - ->with($this->customerData) - ->willReturn($customerName); - - $this->scopeConfig->expects($this->exactly(2)) - ->method('getValue') - ->willReturnMap([ - ['wishlist/email/email_template', ScopeInterface::SCOPE_STORE, null, $templateIdentifier], - ['wishlist/email/email_identity', ScopeInterface::SCOPE_STORE, null, $from], - ]); - - $this->store->expects($this->once()) - ->method('getStoreId') - ->willReturn($storeId); - - $this->url->expects($this->once()) - ->method('getUrl') - ->with('*/shared/index', ['code' => $sharingCode]) - ->willReturn($viewOnSiteLink); - - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->with($templateIdentifier) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateOptions') - ->with([ - 'area' => Area::AREA_FRONTEND, - 'store' => $storeId, - ]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateVars') - ->with([ - 'customer' => $this->customerData, - 'customerName' => $customerName, - 'salable' => 'yes', - 'items' => $text, - 'viewOnSiteLink' => $viewOnSiteLink, - 'message' => $text . $text, - 'store' => $this->store, - ]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setFrom') - ->with($from) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('addTo') - ->with($emails) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('getTransport') - ->willReturn($this->transport); - - $this->transport->expects($this->once()) - ->method('sendMessage') - ->willReturnSelf(); - - $this->eventManager->expects($this->once()) - ->method('dispatch') - ->with('wishlist_share', ['wishlist' => $this->wishlist]) - ->willReturnSelf(); - - $this->messageManager->expects($this->once()) - ->method('addSuccess') - ->with(__('Your wish list has been shared.')) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*', ['wishlist_id' => $wishlistId]) - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } } diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php new file mode 100644 index 0000000000000..fb0113eb6ae75 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Test\Unit\Model\Product; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Wishlist\Model\Product\AttributeValueProvider; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * AttributeValueProviderTest + */ +class AttributeValueProviderTest extends TestCase +{ + /** + * @var AttributeValueProvider|PHPUnit_Framework_MockObject_MockObject + */ + private $attributeValueProvider; + + /** + * @var CollectionFactory|PHPUnit_Framework_MockObject_MockObject + */ + private $productCollectionFactoryMock; + + /** + * @var Product|PHPUnit_Framework_MockObject_MockObject + */ + private $productMock; + + /** + * @var AdapterInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->productCollectionFactoryMock = $this->createPartialMock( + CollectionFactory::class, + ['create'] + ); + $this->attributeValueProvider = new AttributeValueProvider( + $this->productCollectionFactoryMock + ); + } + + /** + * Get attribute text when the flat table is disabled + * + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + * @dataProvider attributeDataProvider + */ + public function testGetAttributeTextWhenFlatIsDisabled(int $productId, string $attributeCode, string $attributeText) + { + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getFirstItem' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(false); + $productCollection->expects($this->any()) + ->method('getFirstItem') + ->willReturn($this->productMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * Get attribute text when the flat table is enabled + * + * @dataProvider attributeDataProvider + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + */ + public function testGetAttributeTextWhenFlatIsEnabled(int $productId, string $attributeCode, string $attributeText) + { + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass(); + $this->connectionMock->expects($this->any()) + ->method('fetchRow') + ->willReturn([ + $attributeCode => $attributeText + ]); + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getConnection' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(true); + $productCollection->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * @return array + */ + public function attributeDataProvider(): array + { + return [ + [1, 'attribute_code', 'Attribute Text'] + ]; + } +} diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php index 98d36dea28a2a..763812ce39dab 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php @@ -19,37 +19,37 @@ class WishlistTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Wishlist\Block\Customer\Wishlist + * @var \Magento\Wishlist\Block\Customer\Wishlist|\PHPUnit_Framework_MockObject_MockObject */ protected $wishlistBlock; /** - * @var \Magento\Rss\Model\RssFactory + * @var \Magento\Rss\Model\RssFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $rssFactoryMock; /** - * @var \Magento\Framework\UrlInterface + * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $urlBuilderMock; /** - * @var \Magento\Wishlist\Helper\Rss + * @var \Magento\Wishlist\Helper\Rss|\PHPUnit_Framework_MockObject_MockObject */ protected $wishlistHelperMock; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $scopeConfig; /** - * @var \Magento\Catalog\Helper\Image + * @var \Magento\Catalog\Helper\Image|\PHPUnit_Framework_MockObject_MockObject */ protected $imageHelperMock; /** - * @var \Magento\Catalog\Helper\Output + * @var \Magento\Catalog\Helper\Output|\PHPUnit_Framework_MockObject_MockObject */ protected $catalogOutputMock; @@ -59,7 +59,7 @@ class WishlistTest extends \PHPUnit\Framework\TestCase protected $layoutMock; /** - * @var \Magento\Customer\Model\CustomerFactory + * @var \Magento\Customer\Model\CustomerFactory|\PHPUnit_Framework_MockObject_MockObject */ protected $customerFactory; @@ -276,17 +276,46 @@ protected function processWishlistItemDescription($wishlistModelMock, $staticArg return $description; } + /** + * @return void + */ public function testIsAllowed() { - $this->scopeConfig->expects($this->once())->method('getValue') + $customerId = 1; + $customerServiceMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + ->setMethods(['getCustomerId']) + ->disableOriginalConstructor() + ->getMock(); + $wishlist->expects($this->once())->method('getCustomerId')->willReturn($customerId); + $this->wishlistHelperMock->expects($this->once())->method('getWishlist') + ->willReturn($wishlist); + $this->wishlistHelperMock->expects($this->once()) + ->method('getCustomer') + ->willReturn($customerServiceMock); + $customerServiceMock->expects($this->once())->method('getId')->willReturn($customerId); + + $this->scopeConfig->expects($this->once())->method('isSetFlag') ->with('rss/wishlist/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) ->will($this->returnValue(true)); + $this->assertTrue($this->model->isAllowed()); } + /** + * @return void + */ public function testGetCacheKey() { - $this->assertEquals('rss_wishlist_data', $this->model->getCacheKey()); + $wishlistId = 1; + $wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + ->setMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $wishlist->expects($this->once())->method('getId')->willReturn($wishlistId); + $this->wishlistHelperMock->expects($this->once())->method('getWishlist')->willReturn($wishlist); + + $this->assertEquals('rss_wishlist_data_1', $this->model->getCacheKey()); } public function testGetCacheLifetime() diff --git a/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php b/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php new file mode 100644 index 0000000000000..993d8817d035c --- /dev/null +++ b/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\ViewModel; + +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\CatalogInventory\Model\StockRegistry; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * ViewModel for Wishlist Cart Block. + */ +class AllowedQuantity implements ArgumentInterface +{ + /** + * @var StockRegistry + */ + private $stockRegistry; + + /** + * @var ItemInterface + */ + private $item; + + /** + * @param StockRegistry $stockRegistry + */ + public function __construct(StockRegistry $stockRegistry) + { + $this->stockRegistry = $stockRegistry; + } + + /** + * Set product configuration item. + * + * @param ItemInterface $item + * @return self + */ + public function setItem(ItemInterface $item): self + { + $this->item = $item; + + return $this; + } + + /** + * Get product configuration item. + * + * @return ItemInterface + */ + public function getItem(): ItemInterface + { + return $this->item; + } + + /** + * Get min and max qty for wishlist form. + * + * @return array + */ + public function getMinMaxQty(): array + { + $product = $this->getItem()->getProduct(); + $stockItem = $this->stockRegistry->getStockItem($product->getId(), $product->getStore()->getWebsiteId()); + $params = []; + + $params['minAllowed'] = (float)$stockItem->getMinSaleQty(); + if ($stockItem->getMaxSaleQty()) { + $params['maxAllowed'] = (float)$stockItem->getMaxSaleQty(); + } else { + $params['maxAllowed'] = (float)StockDataFilter::MAX_QTY_VALUE; + } + + return $params; + } +} diff --git a/app/code/Magento/Wishlist/composer.json b/app/code/Magento/Wishlist/composer.json index 05cf40372517b..e99e0488284b9 100644 --- a/app/code/Magento/Wishlist/composer.json +++ b/app/code/Magento/Wishlist/composer.json @@ -12,7 +12,8 @@ "magento/module-backend": "100.2.*", "magento/module-sales": "101.0.*", "magento/framework": "101.0.*", - "magento/module-ui": "101.0.*" + "magento/module-ui": "101.0.*", + "magento/module-captcha": "100.2.*" }, "suggest": { "magento/module-configurable-product": "100.2.*", @@ -23,7 +24,7 @@ "magento/module-wishlist-sample-data": "Sample Data version:100.2.*" }, "type": "magento2-module", - "version": "101.0.5", + "version": "101.0.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/code/Magento/Wishlist/etc/config.xml b/app/code/Magento/Wishlist/etc/config.xml index 1fec2d1baf9d4..572ecf9de63df 100644 --- a/app/code/Magento/Wishlist/etc/config.xml +++ b/app/code/Magento/Wishlist/etc/config.xml @@ -18,5 +18,22 @@ <text_limit>255</text_limit> </email> </wishlist> + <captcha translate="label"> + <frontend> + <areas> + <share_wishlist_form> + <label>Share Wishlist Form</label> + </share_wishlist_form> + </areas> + </frontend> + </captcha> + <customer> + <captcha> + <shown_to_logged_in_user> + <share_wishlist_form>1</share_wishlist_form> + </shown_to_logged_in_user> + </captcha> + </customer> </default> + </config> diff --git a/app/code/Magento/Wishlist/etc/module.xml b/app/code/Magento/Wishlist/etc/module.xml index ade606be9e086..e36445cfd86e6 100644 --- a/app/code/Magento/Wishlist/etc/module.xml +++ b/app/code/Magento/Wishlist/etc/module.xml @@ -10,6 +10,7 @@ <sequence> <module name="Magento_Customer"/> <module name="Magento_Catalog"/> + <module name="Magento_Captcha"/> </sequence> </module> </config> diff --git a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml index 243a06062425a..05540e313f11d 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml @@ -17,6 +17,7 @@ <block class="Magento\Wishlist\Block\Customer\Wishlist\Items" name="customer.wishlist.items" as="items" template="Magento_Wishlist::item/list.phtml" cacheable="false"> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Image" name="customer.wishlist.item.image" template="Magento_Wishlist::item/column/image.phtml" cacheable="false"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Info" name="customer.wishlist.item.name" template="Magento_Wishlist::item/column/name.phtml" cacheable="false"/> + <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column" name="customer.wishlist.item.review" template="Magento_Wishlist::item/column/review.phtml" cacheable="false"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Cart" name="customer.wishlist.item.price" template="Magento_Wishlist::item/column/price.phtml" cacheable="false"> <block class="Magento\Catalog\Pricing\Render" name="product.price.render.wishlist"> <arguments> @@ -39,6 +40,7 @@ </block> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Cart" name="customer.wishlist.item.cart" template="Magento_Wishlist::item/column/cart.phtml" cacheable="false"> <arguments> + <argument name="allowedQuantityViewModel" xsi:type="object">Magento\Wishlist\ViewModel\AllowedQuantity</argument> <argument name="title" translate="true" xsi:type="string">Add to Cart</argument> </arguments> </block> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml index 49c35331b7868..434bd57a2087f 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml @@ -11,7 +11,9 @@ /** @var \Magento\Wishlist\Model\Item $item */ $item = $block->getItem(); $product = $item->getProduct(); -$allowedQty = $block->getMinMaxQty(); +/** @var \Magento\Wishlist\ViewModel\AllowedQuantity $viewModel */ +$viewModel = $block->getData('allowedQuantityViewModel'); +$allowedQty = $viewModel->setItem($item)->getMinMaxQty(); ?> <?php foreach ($block->getChildNames() as $childName): ?> <?= /* @noEscape */ $block->getLayout()->renderElement($childName, false) ?> @@ -22,8 +24,8 @@ $allowedQty = $block->getMinMaxQty(); <div class="field qty"> <label class="label" for="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> <div class="control"> - <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true, 'validate-item-quantity':{'minAllowed':<?= /* @noEscape */ $allowedQty['minAllowed'] ?>,'maxAllowed':<?= /* @noEscape */ $allowedQty['maxAllowed'] ?>}}" - name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>"> + <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true,'validate-item-quantity':{'minAllowed':<?= /* @noEscape */ $allowedQty['minAllowed'] ?>,'maxAllowed':<?= /* @noEscape */ $allowedQty['maxAllowed'] ?>}}" + name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>" <?= $product->isSaleable() ? '' : 'disabled="disabled"' ?>> </div> </div> <?php endif; ?> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml index 17e2404ee23cf..5ab5bc5422e7e 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml @@ -17,6 +17,6 @@ $item = $block->getItem(); <span><?= $block->escapeHtml(__('Comment')) ?></span> </label> <div class="control"> - <textarea id="product-item-comment-<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>" placeholder="<?= /* @noEscape */ $this->helper('Magento\Wishlist\Helper\Data')->defaultCommentString() ?>" name="description[<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>]" title="<?= $block->escapeHtmlAttr(__('Comment')) ?>" class="product-item-comment"><?= ($block->escapeHtml($item->getDescription())) ?></textarea> + <textarea id="product-item-comment-<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>" placeholder="<?= /* @noEscape */ $this->helper('Magento\Wishlist\Helper\Data')->defaultCommentString() ?>" name="description[<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>]" title="<?= $block->escapeHtmlAttr(__('Comment')) ?>" class="product-item-comment" <?= $item->getProduct()->isSaleable() ? '' : 'disabled="disabled"' ?>><?= ($block->escapeHtml($item->getDescription())) ?></textarea> </div> </div> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml new file mode 100644 index 0000000000000..3fd492233bdd5 --- /dev/null +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Wishlist\Block\Customer\Wishlist\Item\Column $block */ +$product = $block->getItem()->getProduct(); +?> +<?= $block->getReviewsSummaryHtml($product, 'short'); diff --git a/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml b/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml index 430ebd384c82b..ff01cb4532cc7 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml @@ -40,6 +40,7 @@ </div> <?php endif; ?> </fieldset> + <?= $block->getChildHtml('captcha'); ?> <div class="actions-toolbar"> <div class="primary"> <button type="submit" title="<?= $block->escapeHtmlAttr(__('Share Wish List')) ?>" class="action submit primary"> diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index cab130f7c2104..db3b8c83a5064 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -63,6 +63,12 @@ define([ isFileUploaded = false, self = this; + if (event.handleObj.selector == this.options.qtyInfo) { //eslint-disable-line eqeqeq + this._updateAddToWishlistButton({}); + event.stopPropagation(); + + return; + } $(event.handleObj.selector).each(function (index, element) { if ($(element).is('input[type=text]') || $(element).is('input[type=email]') || @@ -83,7 +89,9 @@ define([ } }); - this.bindFormSubmit(isFileUploaded); + if (isFileUploaded) { + this.bindFormSubmit(); + } this._updateAddToWishlistButton(dataToAdd); event.stopPropagation(); }, @@ -154,18 +162,11 @@ define([ $.each(elementValue, function (key, option) { data[elementName + '[' + option + ']'] = option; }); + } else if (elementName.substr(elementName.length - 2) == '[]') { //eslint-disable-line eqeqeq, max-depth + elementName = elementName.substring(0, elementName.length - 2); + data[elementName + '[' + elementValue + ']'] = elementValue; } else { - if (elementValue) { //eslint-disable-line no-lonely-if - if (elementName.substr(elementName.length - 2) == '[]') { //eslint-disable-line eqeqeq, max-depth - elementName = elementName.substring(0, elementName.length - 2); - - if (elementValue) { //eslint-disable-line max-depth - data[elementName + '[' + elementValue + ']'] = elementValue; - } - } else { - data[elementName] = elementValue; - } - } + data[elementName] = elementValue; } return data; @@ -187,45 +188,34 @@ define([ /** * Bind form submit. - * - * @param {Boolean} isFileUploaded */ - bindFormSubmit: function (isFileUploaded) { + bindFormSubmit: function () { var self = this; $('[data-action="add-to-wishlist"]').on('click', function (event) { var element, params, form, action; - if (!$($(self.options.qtyInfo).closest('form')).valid()) { - event.stopPropagation(); - event.preventDefault(); + event.stopPropagation(); + event.preventDefault(); - return; - } + element = $('input[type=file]' + self.options.customOptionsInfo); + params = $(event.currentTarget).data('post'); + form = $(element).closest('form'); + action = params.action; - if (isFileUploaded) { - - element = $('input[type=file]' + self.options.customOptionsInfo); - params = $(event.currentTarget).data('post'); - form = $(element).closest('form'); - action = params.action; - - if (params.data.id) { - $('<input>', { - type: 'hidden', - name: 'id', - value: params.data.id - }).appendTo(form); - } - - if (params.data.uenc) { - action += 'uenc/' + params.data.uenc; - } + if (params.data.id) { + $('<input>', { + type: 'hidden', + name: 'id', + value: params.data.id + }).appendTo(form); + } - $(form).attr('action', action).submit(); - event.stopPropagation(); - event.preventDefault(); + if (params.data.uenc) { + action += 'uenc/' + params.data.uenc; } + + $(form).attr('action', action).submit(); }); } }); diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less index 42c9b289afdb7..0573054d5b96f 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less @@ -271,17 +271,9 @@ &._show { > .submenu { - display: block; - float: left; - left: 100%; - max-width: 1640px; - min-height: 98.65%; - min-width: 100%; - overflow-x: scroll; - position: absolute; transform: translateX(0); visibility: visible; - z-index: 698; + z-index: @submenu__z-index; } } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less index ffbbaeb084162..4cb6bdc59f722 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less @@ -29,6 +29,19 @@ &:extend(.abs-control-qty all); } } + + .admin__field { + &.required, + &._required { + & > .admin__field-label > span { + width: 100%; + + & span:after { + display: none; + } + } + } + } } // diff --git a/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less index c405707ee7bbe..2ae33e0269ac4 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less @@ -3,6 +3,8 @@ // * See COPYING.txt for license details. // */ +@import 'module/_rma.less'; + .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .rma-request-details, .rma-wrapper .order-shipping-address { diff --git a/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/module/_rma.less b/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/module/_rma.less new file mode 100644 index 0000000000000..4b3ee21e04ff3 --- /dev/null +++ b/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/module/_rma.less @@ -0,0 +1,15 @@ +// /** +// * Copyright © Magento, Inc. All rights reserved. +// * See COPYING.txt for license details. +// */ + +// +// Layout +// --------------------------------------------- + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { + .rma-wrapper .order-shipping-method { + float: right; + #mix-grid .width(6,12); + } +} diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less index 2f6aec0315e3b..5bcf4d4953cc6 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less @@ -49,7 +49,7 @@ margin: 0 0 @order-create-sidebar__margin; .lib-typography( @_font-size: 1.9rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__semibold, @_line-height: @line-height__s, @_font-family: false, diff --git a/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less b/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less index a71731320c5ce..a1b22b0e97120 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less +++ b/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less @@ -1,5 +1,5 @@ // /** -// * Copyright © 2015 Magento. All rights reserved. +// * Copyright © Magento, Inc. All rights reserved. // * See COPYING.txt for license details. // */ @@ -16,7 +16,7 @@ @staging-preview-header__font-size: 1.3rem; @staging-preview-header-item__active__background-color: @color-brownie-almost; -@staging-preview-header-item-actions__border-color: @color-darkie-gray; +@staging-preview-header-item-actions__border-color: @color-darker-gray; @staging-preview-form-element__background-color: @color-very-dark-brownie; @staging-preview-form-element__border-color: @color-lighter-grayish-almost; @@ -366,7 +366,7 @@ // Generic data grid .admin__data-grid-outer-wrap { border-top: 1px solid @staging-preview-table-dark__border-color; - max-height: 400px; // ToDO: remove after JS adjustment implemented + max-height: 400px; // ToDO remove after JS adjustment implemented overflow-y: auto; padding: 15px @indent__s 0 0; } diff --git a/app/design/adminhtml/Magento/backend/composer.json b/app/design/adminhtml/Magento/backend/composer.json index e15cd2e5f4271..fb11aa2dc159f 100644 --- a/app/design/adminhtml/Magento/backend/composer.json +++ b/app/design/adminhtml/Magento/backend/composer.json @@ -6,7 +6,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-theme", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less index c535047e37682..c9ad0c6c60b66 100644 --- a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less +++ b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less @@ -10,7 +10,7 @@ @tooltip__background-color: @color-white; @tooltip__border-color: @color-gray68; @tooltip__border-radius: 0; -@tooltip__color: @color-brown-darkie; +@tooltip__color: @color-brown-darker; @tooltip__max-width: 31rem; @tooltip__opacity: .9; @tooltip__shadow-color: @color-gray80; diff --git a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less index 39d7be029f81f..be1378638180f 100644 --- a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less +++ b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less @@ -28,7 +28,7 @@ @color-green-apple: #79a22e; @color-green-islamic: #090; @color-dark-brownie: #41362f; -@color-brown-darkie: #41362f; +@color-brown-darker: #41362f; @color-phoenix-down: #e04f00; @color-phoenix: #eb5202; @color-phoenix-almost-rise: #ef672f; diff --git a/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less b/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less index 911ef55f3f2e6..30500569c82a0 100644 --- a/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less +++ b/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less @@ -15,7 +15,7 @@ @extension-manager-title__background-color: @color-white-fog; @extension-manager-title__border-color: @color-gray89; -@extension-manager-title__color: @color-brown-darkie; +@extension-manager-title__color: @color-brown-darker; @extension-manager-button__border-color: @color-gray68; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less b/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less index 475d3914a5ff0..5658214a76986 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less @@ -45,13 +45,13 @@ } .ui-tabs-anchor { - color: @color-brown-darkie; + color: @color-brown-darker; display: block; padding: 1.5rem 1.8rem 1.3rem; text-decoration: none; &:hover { // ToDo UI: should be deleted with old styles - color: @color-brown-darkie; + color: @color-brown-darker; text-decoration: none; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/_typography.less b/app/design/adminhtml/Magento/backend/web/css/source/_typography.less index 54726d2d34bd9..1f7d7f879c4aa 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/_typography.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/_typography.less @@ -71,7 +71,7 @@ h1 { .lib-typography( @_font-size: 2.8rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__regular, @_line-height: @line-height__s, @_font-family: false, @@ -84,7 +84,7 @@ h2 { .lib-typography( @_font-size: 2rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__regular, @_line-height: @line-height__s, @_font-family: false, @@ -97,7 +97,7 @@ h3 { .lib-typography( @_font-size: 1.7rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__semibold, @_line-height: @line-height__s, @_font-family: false, diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less index 141e5b604e2e2..7e086db5b7d26 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less @@ -48,7 +48,7 @@ @data-grid-file-uploader-menu-button__width: 2rem; -@data-grid-file-uploader-upload-icon__color: @color-darkie-gray; +@data-grid-file-uploader-upload-icon__color: @color-darker-gray; @data-grid-file-uploader-upload-icon__hover__color: @color-very-dark-gray; @data-grid-file-uploader-upload-icon__line-height: 48px; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less index 5879c397e6bee..0218744b80a4d 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less @@ -207,7 +207,6 @@ &:before { .appearing__off(); - content: '.'; margin-left: -7px; overflow: hidden; } @@ -527,6 +526,7 @@ & > .admin__field-label { #mix-grid .column(@field-label-grid__column, @field-grid__columns); + background: @color-white; cursor: pointer; left: 0; position: absolute; @@ -678,10 +678,13 @@ margin: 0; opacity: 1; position: static; - text-align: left; } } + .admin__field-label { + text-align: left; + } + &:nth-child(n + 2) { &:not(.admin__field-option):not(.admin__field-group-show-label):not(.admin__field-date) { > .admin__field-label[class] { diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less index b37266c0de600..ec6fcb1d6b2df 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less @@ -162,7 +162,7 @@ @_icon-font-line-height: 16px, @_icon-font-text-hide: true, @_icon-font-position: after, - @_icon-font-color: @color-brown-darkie + @_icon-font-color: @color-brown-darker ); span { @@ -175,7 +175,7 @@ z-index: 2; &:after { - color: darken(@color-brown-darkie, 20%); + color: darken(@color-brown-darker, 20%); } // @Todo ui - testing solution to show action hint without title attribute @@ -408,6 +408,9 @@ label.mage-error { width: 16px; z-index: 1; + /** + *@codingStandardsIgnoreStart + */ &:before { /** * @codingStandardsIgnoreStart @@ -450,6 +453,7 @@ label.mage-error { &:extend(.admin__control-checkbox:checked + label:before); // @codingStandardsIgnoreEnd } + //@codingStandardsIgnoreEnd } &._indeterminate { @@ -563,7 +567,7 @@ label.mage-error { } .admin__control-select-placeholder { - color: @color-darkie-gray; + color: @color-darker-gray; font-weight: @font-weight__bold; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less b/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less index b477384096b01..ad57d7b47113e 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less @@ -8,7 +8,7 @@ // _____________________________________________ @color-brown-dark: #4a3f39; -@color-brown-darkie: #41362f; +@color-brown-darker: #41362f; @color-very-dark-gray-black: #303030; @color-very-dark-gray-black2: #35302c; @color-very-dark-grayish-orange: #373330; @@ -23,7 +23,7 @@ @color-brownie-vanilla: #736963; @color-dark-gray0: #7f7c7a; @color-dark-gray: #808080; -@color-darkie-gray: #8a837f; +@color-darker-gray: #8a837f; @color-gray65: #a6a6a6; @color-gray65-almost: #a79d95; @color-gray65-lighten: #aaa6a0; @@ -73,5 +73,5 @@ @primary__color: @color-phoenix; @success__color: @color-green-apple; -@text__color: @color-brown-darkie; +@text__color: @color-brown-darker; @border__color: @color-gray89; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less b/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less index 69393a62200cc..40831684adceb 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less @@ -30,7 +30,7 @@ @data-grid-td__odd__update__active__background-color: darken(@data-grid-td__update__active__background-color, 10%); @data-grid-td__odd__update__upcoming__background-color: darken(@data-grid-td__update__upcoming__background-color, 10%); -@data-grid-th__border-color: @color-darkie-gray; +@data-grid-th__border-color: @color-darker-gray; @data-grid-th__border-style: solid; @data-grid-th__background-color: @color-brownie; @data-grid-th__color: @color-white; diff --git a/app/design/frontend/Magento/blank/Magento_CatalogSearch/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_CatalogSearch/web/css/source/_module.less index daed96db717c7..b7255f9792993 100644 --- a/app/design/frontend/Magento/blank/Magento_CatalogSearch/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_CatalogSearch/web/css/source/_module.less @@ -188,6 +188,17 @@ margin-bottom: 0; } } + + .form.search.advanced { + .field.price { + .with-addon { + .input-text { + flex-basis: auto; + width: 100%; + } + } + } + } } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { @@ -256,4 +267,9 @@ .search-autocomplete { margin-top: 0; } + + .form.search.advanced { + min-width: 600px; + width: 50%; + } } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less index 4479c070a4e17..8dec680b58726 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less @@ -55,31 +55,3 @@ } } } - -// -// Desktop -// _____________________________________________ - -.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { - // ToDo UI: remove with global blank theme .field.required update - .opc-wrapper { - .fieldset { - > .field { - &.required, - &._required { - position: relative; - - > label { - padding-right: 25px; - - &:after { - margin-left: @indent__s; - position: absolute; - top: 9px; - } - } - } - } - } - } -} diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less index 5f8134193c67f..35445b0989e86 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -209,6 +209,13 @@ .fieldset { > .field { margin: 0 0 @indent__base; + + &.choice { + &:before { + padding: 0; + width: 0; + } + } &.type { .control { diff --git a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less index 0ebd722429480..2b6eda56331f3 100644 --- a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less @@ -165,7 +165,7 @@ // Checkout address (create shipping address) .field.street { - .field.additional { + .field { .label { &:extend(.abs-visually-hidden all); } diff --git a/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less index f0dd8a957e9b5..6e2069c6e88ef 100644 --- a/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less @@ -55,6 +55,10 @@ } } + .map-fallback-price { + display: none; + } + .map-old-price { text-decoration: none; diff --git a/app/design/frontend/Magento/blank/composer.json b/app/design/frontend/Magento/blank/composer.json index 2f40a5918c8ca..f691cc8e9eb5f 100644 --- a/app/design/frontend/Magento/blank/composer.json +++ b/app/design/frontend/Magento/blank/composer.json @@ -6,7 +6,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-theme", - "version": "100.2.5", + "version": "100.2.6", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/design/frontend/Magento/blank/web/css/source/_forms.less b/app/design/frontend/Magento/blank/web/css/source/_forms.less index c9f3c3d72ef4c..26f5ff89e99e3 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_forms.less +++ b/app/design/frontend/Magento/blank/web/css/source/_forms.less @@ -101,6 +101,18 @@ .lib-form-validation-note(); } + .product-options-wrapper { + .date { + &.required { + div[for*='options'] { + &.mage-error { + display: none !important; + } + } + } + } + } + .field .tooltip { .lib-tooltip(right); .tooltip-content { diff --git a/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less index 0f91f857a715c..cdc9ce2b3be72 100644 --- a/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less @@ -213,6 +213,19 @@ // Mobile // _____________________________________________ +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .form.search.advanced { + .field.price { + .with-addon { + .input-text { + flex-basis: auto; + width: 100%; + } + } + } + } +} + .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { .block-search { margin-top: @indent__s; @@ -275,4 +288,9 @@ .search-autocomplete { margin-top: 0; } + + .form.search.advanced { + min-width: 600px; + width: 50%; + } } diff --git a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less index 460f8e50d59a9..f283a11da3400 100755 --- a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less @@ -199,7 +199,7 @@ // Checkout address (create shipping address) .field.street { - .field.additional { + .field { .label { &:extend(.abs-visually-hidden all); } diff --git a/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less index 112184b45fe86..475361c56afc8 100644 --- a/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less @@ -74,6 +74,10 @@ } } + .map-fallback-price { + display: none; + } + .map-old-price, .product-item .map-old-price, .product-info-price .map-show-info { diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html index 8c4084fcaf496..e467aa843e2f4 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html @@ -51,7 +51,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html index 68f1886986c5b..385110f8f037e 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html @@ -49,7 +49,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less index 48db03d791657..96b8c3442f44a 100644 --- a/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less @@ -164,6 +164,31 @@ } } } + + .products-grid.wishlist { + .product-item-actions { + .action { + &.edit, + &.delete { + .lib-icon-font( + @icon-edit, + @_icon-font-size: 18px, + @_icon-font-line-height: 20px, + @_icon-font-text-hide: true, + @_icon-font-color: @minicart-icons-color, + @_icon-font-color-hover: @primary__color, + @_icon-font-color-active: @minicart-icons-color + ); + } + + &.delete { + .lib-icon-font-symbol( + @_icon-font-content: @icon-trash + ); + } + } + } + } } // @@ -211,15 +236,7 @@ &:last-child { margin-right: 0; } - - &.edit { - float: left; - } - - &.delete { - float: right; - } - + &.edit, &.delete { margin-top: 7px; diff --git a/app/design/frontend/Magento/luma/composer.json b/app/design/frontend/Magento/luma/composer.json index 0a82ce6fdea2e..f5ba64ad9ed57 100644 --- a/app/design/frontend/Magento/luma/composer.json +++ b/app/design/frontend/Magento/luma/composer.json @@ -7,7 +7,7 @@ "magento/framework": "101.0.*" }, "type": "magento2-theme", - "version": "100.2.6", + "version": "100.2.7", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/app/design/frontend/Magento/luma/web/css/source/_forms.less b/app/design/frontend/Magento/luma/web/css/source/_forms.less index 6701d5f9e9d21..fc637384e7a49 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_forms.less +++ b/app/design/frontend/Magento/luma/web/css/source/_forms.less @@ -118,6 +118,18 @@ .lib-form-validation-note(); } + .product-options-wrapper { + .date { + &.required { + div[for*='options'] { + &.mage-error { + display: none !important; + } + } + } + } + } + // TEMP .field .tooltip { diff --git a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less index 0e34d7b87387d..834423912e8a1 100644 --- a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less +++ b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less @@ -59,7 +59,7 @@ .modal-custom { .action-close { - .lib-css(margin, @indent__m); + .lib-css(margin, 15px); } } diff --git a/app/etc/di.xml b/app/etc/di.xml index 05fd34a178ded..5e7d4f67b8b23 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -37,7 +37,7 @@ <preference for="Magento\Framework\Locale\ListsInterface" type="Magento\Framework\Locale\TranslatedLists" /> <preference for="Magento\Framework\Locale\AvailableLocalesInterface" type="Magento\Framework\Locale\Deployed\Codes" /> <preference for="Magento\Framework\Locale\OptionInterface" type="Magento\Framework\Locale\Deployed\Options" /> - <preference for="Magento\Framework\Lock\LockManagerInterface" type="Magento\Framework\Lock\Backend\Database" /> + <preference for="Magento\Framework\Lock\LockManagerInterface" type="Magento\Framework\Lock\Proxy" /> <preference for="Magento\Framework\Api\AttributeTypeResolverInterface" type="Magento\Framework\Reflection\AttributeTypeResolver" /> <preference for="Magento\Framework\Api\Search\SearchResultInterface" type="Magento\Framework\Api\Search\SearchResult" /> <preference for="Magento\Framework\Api\Search\SearchCriteriaInterface" type="Magento\Framework\Api\Search\SearchCriteria"/> diff --git a/composer.json b/composer.json index d04bf269d4485..8df1d26295124 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento2ce", "description": "Magento 2 (Open Source)", "type": "project", - "version": "2.2.8-dev", + "version": "2.2.9-dev", "license": [ "OSL-3.0", "AFL-3.0" @@ -88,124 +88,125 @@ }, "replace": { "magento/module-marketplace": "100.2.4", - "magento/module-admin-notification": "100.2.5", + "magento/module-admin-notification": "100.2.6", "magento/module-advanced-pricing-import-export": "100.2.5", - "magento/module-analytics": "100.2.4", + "magento/module-analytics": "100.2.5", "magento/module-authorization": "100.2.3", - "magento/module-authorizenet": "100.2.3", - "magento/module-backend": "100.2.7", - "magento/module-backup": "100.2.6", - "magento/module-braintree": "100.2.7", - "magento/module-bundle": "100.2.6", - "magento/module-bundle-import-export": "100.2.4", - "magento/module-cache-invalidate": "100.2.3", - "magento/module-captcha": "100.2.4", - "magento/module-catalog": "102.0.7", + "magento/module-authorizenet": "100.2.4", + "magento/module-backend": "100.2.8", + "magento/module-backup": "100.2.7", + "magento/module-braintree": "100.2.8", + "magento/module-bundle": "100.2.7", + "magento/module-bundle-import-export": "100.2.5", + "magento/module-cache-invalidate": "100.2.4", + "magento/module-captcha": "100.2.5", + "magento/module-catalog": "102.0.8", "magento/module-catalog-analytics": "100.2.3", - "magento/module-catalog-import-export": "100.2.6", - "magento/module-catalog-inventory": "100.2.6", - "magento/module-catalog-rule": "101.0.6", + "magento/module-catalog-import-export": "100.2.7", + "magento/module-catalog-inventory": "100.2.7", + "magento/module-catalog-rule": "101.0.7", "magento/module-catalog-rule-configurable": "100.2.3", - "magento/module-catalog-search": "100.2.6", - "magento/module-catalog-url-rewrite": "100.2.6", - "magento/module-catalog-widget": "100.2.4", - "magento/module-checkout": "100.2.7", - "magento/module-checkout-agreements": "100.2.3", - "magento/module-cms": "102.0.7", + "magento/module-catalog-search": "100.2.7", + "magento/module-catalog-url-rewrite": "100.2.7", + "magento/module-catalog-widget": "100.2.5", + "magento/module-checkout": "100.2.8", + "magento/module-checkout-agreements": "100.2.4", + "magento/module-cms": "102.0.8", "magento/module-cms-url-rewrite": "100.2.3", - "magento/module-config": "101.0.7", - "magento/module-configurable-import-export": "100.2.4", - "magento/module-configurable-product": "100.2.7", + "magento/module-config": "101.0.8", + "magento/module-configurable-import-export": "100.2.5", + "magento/module-configurable-product": "100.2.8", "magento/module-configurable-product-sales": "100.2.4", - "magento/module-contact": "100.2.4", + "magento/module-contact": "100.2.5", "magento/module-cookie": "100.2.3", - "magento/module-cron": "100.2.5", - "magento/module-currency-symbol": "100.2.3", - "magento/module-customer": "101.0.7", + "magento/module-cron": "100.2.6", + "magento/module-currency-symbol": "100.2.4", + "magento/module-customer": "101.0.8", "magento/module-customer-analytics": "100.2.3", - "magento/module-customer-import-export": "100.2.5", - "magento/module-deploy": "100.2.6", - "magento/module-developer": "100.2.5", + "magento/module-customer-import-export": "100.2.6", + "magento/module-deploy": "100.2.7", + "magento/module-developer": "100.2.6", "magento/module-dhl": "100.2.4", - "magento/module-directory": "100.2.6", - "magento/module-downloadable": "100.2.6", + "magento/module-directory": "100.2.7", + "magento/module-downloadable": "100.2.7", "magento/module-downloadable-import-export": "100.2.3", - "magento/module-eav": "101.0.6", - "magento/module-email": "100.2.5", + "magento/module-eav": "101.0.7", + "magento/module-email": "100.2.6", "magento/module-encryption-key": "100.2.3", "magento/module-fedex": "100.2.4", - "magento/module-gift-message": "100.2.3", + "magento/module-gift-message": "100.2.4", "magento/module-google-adwords": "100.2.3", - "magento/module-google-analytics": "100.2.5", + "magento/module-google-analytics": "100.2.6", "magento/module-google-optimizer": "100.2.4", - "magento/module-grouped-import-export": "100.2.3", - "magento/module-grouped-product": "100.2.5", - "magento/module-import-export": "100.2.7", - "magento/module-indexer": "100.2.5", - "magento/module-instant-purchase": "100.2.3", - "magento/module-integration": "100.2.5", - "magento/module-layered-navigation": "100.2.4", + "magento/module-grouped-import-export": "100.2.4", + "magento/module-grouped-product": "100.2.6", + "magento/module-import-export": "100.2.8", + "magento/module-indexer": "100.2.6", + "magento/module-instant-purchase": "100.2.4", + "magento/module-integration": "100.2.6", + "magento/module-layered-navigation": "100.2.5", "magento/module-media-storage": "100.2.3", - "magento/module-msrp": "100.2.3", - "magento/module-multishipping": "100.2.4", - "magento/module-new-relic-reporting": "100.2.5", - "magento/module-newsletter": "100.2.6", + "magento/module-msrp": "100.2.4", + "magento/module-multishipping": "100.2.5", + "magento/module-new-relic-reporting": "100.2.6", + "magento/module-newsletter": "100.2.7", "magento/module-offline-payments": "100.2.3", - "magento/module-offline-shipping": "100.2.5", - "magento/module-page-cache": "100.2.4", - "magento/module-payment": "100.2.5", - "magento/module-paypal": "100.2.5", - "magento/module-persistent": "100.2.3", + "magento/module-offline-shipping": "100.2.6", + "magento/module-page-cache": "100.2.5", + "magento/module-payment": "100.2.6", + "magento/module-paypal": "100.2.6", + "magento/module-paypal-captcha": "100.2.0", + "magento/module-persistent": "100.2.4", "magento/module-product-alert": "100.2.4", - "magento/module-product-video": "100.2.5", - "magento/module-quote": "101.0.6", + "magento/module-product-video": "100.2.6", + "magento/module-quote": "101.0.7", "magento/module-quote-analytics": "100.2.3", "magento/module-release-notification": "100.2.4", - "magento/module-reports": "100.2.7", + "magento/module-reports": "100.2.8", "magento/module-require-js": "100.2.4", - "magento/module-review": "100.2.7", + "magento/module-review": "100.2.8", "magento/module-review-analytics": "100.2.3", "magento/module-robots": "100.2.4", "magento/module-rss": "100.2.3", - "magento/module-rule": "100.2.4", - "magento/module-sales": "101.0.6", + "magento/module-rule": "100.2.5", + "magento/module-sales": "101.0.7", "magento/module-sales-analytics": "100.2.3", "magento/module-sales-inventory": "100.2.3", - "magento/module-sales-rule": "101.0.5", + "magento/module-sales-rule": "101.0.6", "magento/module-sales-sequence": "100.2.3", "magento/module-sample-data": "100.2.5", - "magento/module-search": "100.2.6", - "magento/module-security": "100.2.4", - "magento/module-send-friend": "100.2.3", - "magento/module-shipping": "100.2.7", - "magento/module-signifyd": "100.2.4", - "magento/module-sitemap": "100.2.6", - "magento/module-store": "100.2.6", + "magento/module-search": "100.2.7", + "magento/module-security": "100.2.5", + "magento/module-send-friend": "100.2.4", + "magento/module-shipping": "100.2.8", + "magento/module-signifyd": "100.2.5", + "magento/module-sitemap": "100.2.7", + "magento/module-store": "100.2.7", "magento/module-swagger-webapi": "100.2.1", "magento/module-swagger": "100.2.5", - "magento/module-swatches": "100.2.5", + "magento/module-swatches": "100.2.6", "magento/module-swatches-layered-navigation": "100.2.3", - "magento/module-tax": "100.2.7", + "magento/module-tax": "100.2.8", "magento/module-tax-import-export": "100.2.3", - "magento/module-theme": "100.2.7", + "magento/module-theme": "100.2.8", "magento/module-translation": "100.2.6", - "magento/module-ui": "101.0.7", - "magento/module-ups": "100.2.5", - "magento/module-url-rewrite": "101.0.6", - "magento/module-user": "101.0.5", - "magento/module-usps": "100.2.5", + "magento/module-ui": "101.0.8", + "magento/module-ups": "100.2.6", + "magento/module-url-rewrite": "101.0.7", + "magento/module-user": "101.0.6", + "magento/module-usps": "100.2.6", "magento/module-variable": "100.2.6", "magento/module-vault": "101.0.5", "magento/module-version": "100.2.3", - "magento/module-webapi": "100.2.5", + "magento/module-webapi": "100.2.6", "magento/module-webapi-security": "100.2.4", - "magento/module-weee": "100.2.4", - "magento/module-widget": "101.0.5", - "magento/module-wishlist": "101.0.5", + "magento/module-weee": "100.2.5", + "magento/module-widget": "101.0.6", + "magento/module-wishlist": "101.0.6", "magento/module-wishlist-analytics": "100.2.3", - "magento/theme-adminhtml-backend": "100.2.5", - "magento/theme-frontend-blank": "100.2.5", - "magento/theme-frontend-luma": "100.2.6", + "magento/theme-adminhtml-backend": "100.2.6", + "magento/theme-frontend-blank": "100.2.6", + "magento/theme-frontend-luma": "100.2.7", "magento/language-de_de": "100.2.0", "magento/language-en_us": "100.2.0", "magento/language-es_es": "100.2.0", @@ -213,7 +214,7 @@ "magento/language-nl_nl": "100.2.0", "magento/language-pt_br": "100.2.0", "magento/language-zh_hans_cn": "100.2.0", - "magento/framework": "101.0.7", + "magento/framework": "101.0.8", "trentrichardson/jquery-timepicker-addon": "1.4.3", "components/jquery": "1.11.0", "blueimp/jquery-file-upload": "5.6.14", diff --git a/composer.lock b/composer.lock index 384ff14618a80..acdfe237272f1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d57f148fd874b8685bf73bbf0a0ea75a", + "content-hash": "442a108524a23e16cbb70c8957cd3097", "packages": [ { "name": "braintree/braintree_php", @@ -204,16 +204,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.1.2", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0" + "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/46afded9720f40b9dc63542af4e3e43a1177acb0", - "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/558f321c52faeb4828c03e7dc0cfe39a09e09a2d", + "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d", "shasum": "" }, "require": { @@ -256,7 +256,7 @@ "ssl", "tls" ], - "time": "2018-08-08T08:57:40+00:00" + "time": "2019-01-28T09:30:10+00:00" }, { "name": "composer/composer", @@ -337,16 +337,16 @@ }, { "name": "composer/semver", - "version": "1.4.2", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", + "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e", + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e", "shasum": "" }, "require": { @@ -395,28 +395,27 @@ "validation", "versioning" ], - "time": "2016-08-30T16:08:34+00:00" + "time": "2019-03-19T17:25:45+00:00" }, { "name": "composer/spdx-licenses", - "version": "1.4.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/composer/spdx-licenses.git", - "reference": "cb17687e9f936acd7e7245ad3890f953770dec1b" + "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/cb17687e9f936acd7e7245ad3890f953770dec1b", - "reference": "cb17687e9f936acd7e7245ad3890f953770dec1b", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d", + "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", - "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7" }, "type": "library", "extra": { @@ -456,7 +455,7 @@ "spdx", "validator" ], - "time": "2018-04-30T10:33:04+00:00" + "time": "2019-03-26T10:23:26+00:00" }, { "name": "container-interop/container-interop", @@ -491,23 +490,23 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.2.7", + "version": "5.2.8", "source": { "type": "git", "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "8560d4314577199ba51bf2032f02cd1315587c23" + "reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23", - "reference": "8560d4314577199ba51bf2032f02cd1315587c23", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/dcb6e1006bb5fd1e392b4daa68932880f37550d4", + "reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.1", + "friendsofphp/php-cs-fixer": "~2.2.20", "json-schema/json-schema-test-suite": "1.2.0", "phpunit/phpunit": "^4.8.35" }, @@ -553,7 +552,7 @@ "json", "schema" ], - "time": "2018-02-14T22:26:30+00:00" + "time": "2019-01-14T23:55:14+00:00" }, { "name": "magento/composer", @@ -719,16 +718,16 @@ }, { "name": "monolog/monolog", - "version": "1.23.0", + "version": "1.24.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", "shasum": "" }, "require": { @@ -793,7 +792,7 @@ "logging", "psr-3" ], - "time": "2017-06-19T01:22:40+00:00" + "time": "2018-11-05T09:00:11+00:00" }, { "name": "oyejorge/less.php", @@ -859,16 +858,16 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.17", + "version": "v2.0.18", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d" + "reference": "0a58ef6e3146256cc3dc7cc393927bcc7d1b72db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d", - "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/0a58ef6e3146256cc3dc7cc393927bcc7d1b72db", + "reference": "0a58ef6e3146256cc3dc7cc393927bcc7d1b72db", "shasum": "" }, "require": { @@ -904,28 +903,33 @@ "pseudorandom", "random" ], - "time": "2018-07-04T16:31:37+00:00" + "time": "2019-01-03T20:59:08+00:00" }, { "name": "pelago/emogrifier", - "version": "v2.0.0", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/MyIntervals/emogrifier.git", - "reference": "8babf8ddbf348f26b29674e2f84db66ff7e3d95e" + "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/8babf8ddbf348f26b29674e2f84db66ff7e3d95e", - "reference": "8babf8ddbf348f26b29674e2f84db66ff7e3d95e", + "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/8ee7fb5ad772915451ed3415c1992bd3697d4983", + "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983", "shasum": "" }, "require": { - "php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0" + "ext-dom": "*", + "ext-libxml": "*", + "php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0", + "symfony/css-selector": "^3.4.0 || ^4.0.0" }, "require-dev": { + "friendsofphp/php-cs-fixer": "^2.2.0", + "phpmd/phpmd": "^2.6.0", "phpunit/phpunit": "^4.8.0", - "squizlabs/php_codesniffer": "^3.1.0" + "squizlabs/php_codesniffer": "^3.3.2" }, "type": "library", "extra": { @@ -935,7 +939,7 @@ }, "autoload": { "psr-4": { - "Pelago\\": "Classes/" + "Pelago\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -953,10 +957,6 @@ { "name": "Jaime Prado" }, - { - "name": "Roman Ožana", - "email": "ozana@omdesign.cz" - }, { "name": "Oliver Klee", "email": "github@oliverklee.de" @@ -964,6 +964,10 @@ { "name": "Zoli Szabó", "email": "zoli.szabo+github@gmail.com" + }, + { + "name": "Jake Hotson", + "email": "jake@qzdesign.co.uk" } ], "description": "Converts CSS styles into inline style attributes in your HTML code", @@ -973,20 +977,20 @@ "email", "pre-processing" ], - "time": "2018-01-05T23:30:21+00:00" + "time": "2018-12-10T10:36:30+00:00" }, { "name": "phpseclib/phpseclib", - "version": "2.0.11", + "version": "2.0.15", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "7053f06f91b3de78e143d430e55a8f7889efc08b" + "reference": "11cf67cf78dc4acb18dc9149a57be4aee5036ce0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/7053f06f91b3de78e143d430e55a8f7889efc08b", - "reference": "7053f06f91b3de78e143d430e55a8f7889efc08b", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/11cf67cf78dc4acb18dc9149a57be4aee5036ce0", + "reference": "11cf67cf78dc4acb18dc9149a57be4aee5036ce0", "shasum": "" }, "require": { @@ -1065,7 +1069,7 @@ "x.509", "x509" ], - "time": "2018-04-15T16:55:05+00:00" + "time": "2019-03-10T16:53:45+00:00" }, { "name": "psr/container", @@ -1168,16 +1172,16 @@ }, { "name": "psr/log", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", "shasum": "" }, "require": { @@ -1211,7 +1215,7 @@ "psr", "psr-3" ], - "time": "2016-10-10T12:19:37+00:00" + "time": "2018-11-20T15:27:04+00:00" }, { "name": "ramsey/uuid", @@ -1436,16 +1440,16 @@ }, { "name": "symfony/console", - "version": "v2.8.46", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "aca0dcc0c75496e17e2aa0303bb9c8e6d79ed789" + "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/aca0dcc0c75496e17e2aa0303bb9c8e6d79ed789", - "reference": "aca0dcc0c75496e17e2aa0303bb9c8e6d79ed789", + "url": "https://api.github.com/repos/symfony/console/zipball/cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12", + "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12", "shasum": "" }, "require": { @@ -1493,7 +1497,60 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-09-30T03:33:07+00:00" + "time": "2018-11-20T15:55:20+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v3.4.23", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/8ca29297c29b64fb3a1a135e71cb25f67f9fdccf", + "reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2019-01-16T09:39:14+00:00" }, { "name": "symfony/debug", @@ -1554,16 +1611,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.46", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "84ae343f39947aa084426ed1138bb96bf94d1f12" + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/84ae343f39947aa084426ed1138bb96bf94d1f12", - "reference": "84ae343f39947aa084426ed1138bb96bf94d1f12", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a77e974a5fecb4398833b0709210e3d5e334ffb0", + "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0", "shasum": "" }, "require": { @@ -1610,20 +1667,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-07-26T09:03:18+00:00" + "time": "2018-11-21T14:20:20+00:00" }, { "name": "symfony/filesystem", - "version": "v3.4.17", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d69930fc337d767607267d57c20a7403d0a822a4" + "reference": "acf99758b1df8e9295e6b85aa69f294565c9fedb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d69930fc337d767607267d57c20a7403d0a822a4", - "reference": "d69930fc337d767607267d57c20a7403d0a822a4", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/acf99758b1df8e9295e6b85aa69f294565c9fedb", + "reference": "acf99758b1df8e9295e6b85aa69f294565c9fedb", "shasum": "" }, "require": { @@ -1660,20 +1717,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-10-02T12:28:39+00:00" + "time": "2019-02-04T21:34:32+00:00" }, { "name": "symfony/finder", - "version": "v3.4.17", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d" + "reference": "fcdde4aa38f48190ce70d782c166f23930084f9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/54ba444dddc5bd5708a34bd095ea67c6eb54644d", - "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d", + "url": "https://api.github.com/repos/symfony/finder/zipball/fcdde4aa38f48190ce70d782c166f23930084f9b", + "reference": "fcdde4aa38f48190ce70d782c166f23930084f9b", "shasum": "" }, "require": { @@ -1709,20 +1766,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-10-03T08:46:40+00:00" + "time": "2019-02-22T14:44:53+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + "reference": "82ebae02209c21113908c229e9883c419720738a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", + "reference": "82ebae02209c21113908c229e9883c419720738a", "shasum": "" }, "require": { @@ -1734,7 +1791,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -1767,20 +1824,20 @@ "polyfill", "portable" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", "shasum": "" }, "require": { @@ -1792,7 +1849,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -1826,20 +1883,20 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/process", - "version": "v2.8.46", + "version": "v2.8.49", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f09e21b7c5aba06c47bbfad9cbcf13ac7f0db0a6" + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f09e21b7c5aba06c47bbfad9cbcf13ac7f0db0a6", - "reference": "f09e21b7c5aba06c47bbfad9cbcf13ac7f0db0a6", + "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8", + "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8", "shasum": "" }, "require": { @@ -1875,7 +1932,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-09-06T17:11:15+00:00" + "time": "2018-11-11T11:18:13+00:00" }, { "name": "tedivm/jshrink", @@ -2294,16 +2351,16 @@ }, { "name": "zendframework/zend-db", - "version": "2.9.3", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-db.git", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9" + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-db/zipball/5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", + "url": "https://api.github.com/repos/zendframework/zend-db/zipball/77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", "shasum": "" }, "require": { @@ -2314,7 +2371,7 @@ "phpunit/phpunit": "^5.7.25 || ^6.4.4", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-hydrator": "^1.1 || ^2.1", + "zendframework/zend-hydrator": "^1.1 || ^2.1 || ^3.0", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" }, "suggest": { @@ -2348,7 +2405,7 @@ "db", "zf" ], - "time": "2018-04-09T13:21:36+00:00" + "time": "2019-02-25T11:37:45+00:00" }, { "name": "zendframework/zend-di", @@ -2555,16 +2612,16 @@ }, { "name": "zendframework/zend-filter", - "version": "2.8.0", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "7b997dbe79459f1652deccc8786d7407fb66caa9" + "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/7b997dbe79459f1652deccc8786d7407fb66caa9", - "reference": "7b997dbe79459f1652deccc8786d7407fb66caa9", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", + "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", "shasum": "" }, "require": { @@ -2577,12 +2634,14 @@ "require-dev": { "pear/archive_tar": "^1.4.3", "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "psr/http-factory": "^1.0", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-crypt": "^3.2.1", "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", "zendframework/zend-uri": "^2.6" }, "suggest": { + "psr/http-factory-implementation": "psr/http-factory-implementation, for creating file upload instances when consuming PSR-7 in file upload filters", "zendframework/zend-crypt": "Zend\\Crypt component, for encryption filters", "zendframework/zend-i18n": "Zend\\I18n component for filters depending on i18n functionality", "zendframework/zend-servicemanager": "Zend\\ServiceManager component, for using the filter chain functionality", @@ -2591,8 +2650,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\Filter", @@ -2614,25 +2673,25 @@ "filter", "zf" ], - "time": "2018-04-11T16:20:04+00:00" + "time": "2018-12-17T16:00:04+00:00" }, { "name": "zendframework/zend-form", - "version": "2.12.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-form.git", - "reference": "565fb4f4bb3e0dbeea0173c923c4a8be77de9441" + "reference": "c713a12ccbd43148b71c9339e171ca11e3f8a1da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-form/zipball/565fb4f4bb3e0dbeea0173c923c4a8be77de9441", - "reference": "565fb4f4bb3e0dbeea0173c923c4a8be77de9441", + "url": "https://api.github.com/repos/zendframework/zend-form/zipball/c713a12ccbd43148b71c9339e171ca11e3f8a1da", + "reference": "c713a12ccbd43148b71c9339e171ca11e3f8a1da", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", - "zendframework/zend-hydrator": "^1.1 || ^2.1", + "zendframework/zend-hydrator": "^1.1 || ^2.1 || ^3.0", "zendframework/zend-inputfilter": "^2.8", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, @@ -2666,8 +2725,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.12.x-dev", - "dev-develop": "2.13.x-dev" + "dev-master": "2.13.x-dev", + "dev-develop": "2.14.x-dev" }, "zf": { "component": "Zend\\Form", @@ -2692,20 +2751,20 @@ "form", "zf" ], - "time": "2018-05-16T18:49:44+00:00" + "time": "2018-12-11T22:51:29+00:00" }, { "name": "zendframework/zend-http", - "version": "2.8.2", + "version": "2.8.4", "source": { "type": "git", "url": "https://github.com/zendframework/zend-http.git", - "reference": "2c8aed3d25522618573194e7cc51351f8cd4a45b" + "reference": "d160aedc096be230af0fe9c31151b2b33ad4e807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-http/zipball/2c8aed3d25522618573194e7cc51351f8cd4a45b", - "reference": "2c8aed3d25522618573194e7cc51351f8cd4a45b", + "url": "https://api.github.com/repos/zendframework/zend-http/zipball/d160aedc096be230af0fe9c31151b2b33ad4e807", + "reference": "d160aedc096be230af0fe9c31151b2b33ad4e807", "shasum": "" }, "require": { @@ -2747,7 +2806,7 @@ "zend", "zf" ], - "time": "2018-08-13T18:47:03+00:00" + "time": "2019-02-07T17:47:08+00:00" }, { "name": "zendframework/zend-hydrator", @@ -2877,34 +2936,38 @@ }, { "name": "zendframework/zend-inputfilter", - "version": "2.8.2", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-inputfilter.git", - "reference": "3f02179e014d9ef0faccda2ad6c65d38adc338d8" + "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/3f02179e014d9ef0faccda2ad6c65d38adc338d8", - "reference": "3f02179e014d9ef0faccda2ad6c65d38adc338d8", + "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", + "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", - "zendframework/zend-filter": "^2.6", + "zendframework/zend-filter": "^2.9.1", "zendframework/zend-servicemanager": "^2.7.10 || ^3.3.1", "zendframework/zend-stdlib": "^2.7 || ^3.0", - "zendframework/zend-validator": "^2.10.1" + "zendframework/zend-validator": "^2.11" }, "require-dev": { "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "psr/http-message": "^1.0", "zendframework/zend-coding-standard": "~1.0.0" }, + "suggest": { + "psr/http-message-implementation": "PSR-7 is required if you wish to validate PSR-7 UploadedFileInterface payloads" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.10.x-dev", + "dev-develop": "2.11.x-dev" }, "zf": { "component": "Zend\\InputFilter", @@ -2926,7 +2989,7 @@ "inputfilter", "zf" ], - "time": "2018-05-14T17:38:03+00:00" + "time": "2019-01-30T16:58:51+00:00" }, { "name": "zendframework/zend-json", @@ -3163,16 +3226,16 @@ }, { "name": "zendframework/zend-math", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-math.git", - "reference": "f4358090d5d23973121f1ed0b376184b66d9edec" + "reference": "1abce074004dacac1a32cd54de94ad47ef960d38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-math/zipball/f4358090d5d23973121f1ed0b376184b66d9edec", - "reference": "f4358090d5d23973121f1ed0b376184b66d9edec", + "url": "https://api.github.com/repos/zendframework/zend-math/zipball/1abce074004dacac1a32cd54de94ad47ef960d38", + "reference": "1abce074004dacac1a32cd54de94ad47ef960d38", "shasum": "" }, "require": { @@ -3209,7 +3272,7 @@ "math", "zf2" ], - "time": "2016-04-07T16:29:53+00:00" + "time": "2018-12-04T15:34:17+00:00" }, { "name": "zendframework/zend-mime", @@ -3851,16 +3914,16 @@ }, { "name": "zendframework/zend-uri", - "version": "2.6.1", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-uri.git", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f" + "reference": "b2785cd38fe379a784645449db86f21b7739b1ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/3b6463645c6766f78ce537c70cb4fdabee1e725f", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f", + "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/b2785cd38fe379a784645449db86f21b7739b1ee", + "reference": "b2785cd38fe379a784645449db86f21b7739b1ee", "shasum": "" }, "require": { @@ -3875,8 +3938,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6.x-dev", - "dev-develop": "2.7.x-dev" + "dev-master": "2.7.x-dev", + "dev-develop": "2.8.x-dev" } }, "autoload": { @@ -3894,20 +3957,20 @@ "uri", "zf" ], - "time": "2018-04-30T13:40:08+00:00" + "time": "2019-02-27T21:39:04+00:00" }, { "name": "zendframework/zend-validator", - "version": "2.10.2", + "version": "2.11.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-validator.git", - "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9" + "reference": "3c28dfe4e5951ba38059cea895244d9d206190b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9", - "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9", + "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/3c28dfe4e5951ba38059cea895244d9d206190b3", + "reference": "3c28dfe4e5951ba38059cea895244d9d206190b3", "shasum": "" }, "require": { @@ -3917,6 +3980,7 @@ }, "require-dev": { "phpunit/phpunit": "^6.0.8 || ^5.7.15", + "psr/http-message": "^1.0", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-config": "^2.6", @@ -3930,6 +3994,7 @@ "zendframework/zend-uri": "^2.5" }, "suggest": { + "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators", "zendframework/zend-db": "Zend\\Db component, required by the (No)RecordExists validator", "zendframework/zend-filter": "Zend\\Filter component, required by the Digits validator", "zendframework/zend-i18n": "Zend\\I18n component to allow translation of validation error messages", @@ -3942,8 +4007,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" + "dev-master": "2.11.x-dev", + "dev-develop": "2.12.x-dev" }, "zf": { "component": "Zend\\Validator", @@ -3965,25 +4030,26 @@ "validator", "zf2" ], - "time": "2018-02-01T17:05:33+00:00" + "time": "2019-01-29T22:26:39+00:00" }, { "name": "zendframework/zend-view", - "version": "2.10.0", + "version": "2.11.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-view.git", - "reference": "4478cc5dd960e2339d88b363ef99fa278700e80e" + "reference": "4f5cb653ed4c64bb8d9bf05b294300feb00c67f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-view/zipball/4478cc5dd960e2339d88b363ef99fa278700e80e", - "reference": "4478cc5dd960e2339d88b363ef99fa278700e80e", + "url": "https://api.github.com/repos/zendframework/zend-view/zipball/4f5cb653ed4c64bb8d9bf05b294300feb00c67f2", + "reference": "4f5cb653ed4c64bb8d9bf05b294300feb00c67f2", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", + "zendframework/zend-json": "^2.6.1 || ^3.0", "zendframework/zend-loader": "^2.5", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, @@ -3999,10 +4065,9 @@ "zendframework/zend-filter": "^2.6.1", "zendframework/zend-http": "^2.5.4", "zendframework/zend-i18n": "^2.6", - "zendframework/zend-json": "^2.6.1", "zendframework/zend-log": "^2.7", "zendframework/zend-modulemanager": "^2.7.1", - "zendframework/zend-mvc": "^2.7 || ^3.0", + "zendframework/zend-mvc": "^2.7.14 || ^3.0", "zendframework/zend-navigation": "^2.5", "zendframework/zend-paginator": "^2.5", "zendframework/zend-permissions-acl": "^2.6", @@ -4019,8 +4084,8 @@ "zendframework/zend-filter": "Zend\\Filter component", "zendframework/zend-http": "Zend\\Http component", "zendframework/zend-i18n": "Zend\\I18n component", - "zendframework/zend-json": "Zend\\Json component", "zendframework/zend-mvc": "Zend\\Mvc component", + "zendframework/zend-mvc-plugin-flashmessenger": "zend-mvc-plugin-flashmessenger component, if you want to use the FlashMessenger view helper with zend-mvc versions 3 and up", "zendframework/zend-navigation": "Zend\\Navigation component", "zendframework/zend-paginator": "Zend\\Paginator component", "zendframework/zend-permissions-acl": "Zend\\Permissions\\Acl component", @@ -4033,8 +4098,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" + "dev-master": "2.11.x-dev", + "dev-develop": "2.12.x-dev" } }, "autoload": { @@ -4052,7 +4117,7 @@ "view", "zf2" ], - "time": "2018-01-17T22:21:50+00:00" + "time": "2019-02-19T17:40:15+00:00" } ], "packages-dev": [ @@ -4161,16 +4226,16 @@ }, { "name": "behat/gherkin", - "version": "v4.4.5", + "version": "v4.6.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" + "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/ab0a02ea14893860bca00f225f5621d351a3ad07", + "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07", "shasum": "" }, "require": { @@ -4178,8 +4243,8 @@ }, "require-dev": { "phpunit/phpunit": "~4.5|~5", - "symfony/phpunit-bridge": "~2.7|~3", - "symfony/yaml": "~2.3|~3" + "symfony/phpunit-bridge": "~2.7|~3|~4", + "symfony/yaml": "~2.3|~3|~4" }, "suggest": { "symfony/yaml": "If you want to parse features, represented in YAML files" @@ -4216,35 +4281,32 @@ "gherkin", "parser" ], - "time": "2016-10-30T11:50:56+00:00" + "time": "2019-01-16T14:22:17+00:00" }, { "name": "codeception/codeception", - "version": "2.3.9", + "version": "2.4.5", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e" + "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/104f46fa0bde339f1bcc3a375aac21eb36e65a1e", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5fee32d5c82791548931cbc34806b4de6aa1abfc", + "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc", "shasum": "" }, "require": { - "behat/gherkin": "~4.4.0", - "codeception/stub": "^1.0", + "behat/gherkin": "^4.4.0", + "codeception/phpunit-wrapper": "^6.0.9|^7.0.6", + "codeception/stub": "^2.0", "ext-json": "*", "ext-mbstring": "*", "facebook/webdriver": ">=1.1.3 <2.0", "guzzlehttp/guzzle": ">=4.1.4 <7.0", "guzzlehttp/psr7": "~1.0", - "php": ">=5.4.0 <8.0", - "phpunit/php-code-coverage": ">=2.2.4 <6.0", - "phpunit/phpunit": ">=4.8.28 <5.0.0 || >=5.6.3 <7.0", - "sebastian/comparator": ">1.1 <3.0", - "sebastian/diff": ">=1.4 <3.0", + "php": ">=5.6.0 <8.0", "symfony/browser-kit": ">=2.7 <5.0", "symfony/console": ">=2.7 <5.0", "symfony/css-selector": ">=2.7 <5.0", @@ -4310,27 +4372,70 @@ "functional testing", "unit testing" ], - "time": "2018-02-26T23:29:41+00:00" + "time": "2018-08-01T07:21:49+00:00" }, { - "name": "codeception/stub", - "version": "1.0.4", + "name": "codeception/phpunit-wrapper", + "version": "6.0.16", "source": { "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805" + "url": "https://github.com/Codeception/phpunit-wrapper.git", + "reference": "299e3aece31489ed962e6c39fe2fb6f3bbd2eb16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/681b62348837a5ef07d10d8a226f5bc358cc8805", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/299e3aece31489ed962e6c39fe2fb6f3bbd2eb16", + "reference": "299e3aece31489ed962e6c39fe2fb6f3bbd2eb16", "shasum": "" }, "require": { - "phpunit/phpunit-mock-objects": ">2.3 <7.0" + "phpunit/php-code-coverage": ">=4.0.4 <6.0", + "phpunit/phpunit": ">=5.7.27 <6.5.13", + "sebastian/comparator": ">=1.2.4 <3.0", + "sebastian/diff": ">=1.4 <4.0" + }, + "replace": { + "codeception/phpunit-wrapper": "*" }, "require-dev": { - "phpunit/phpunit": ">=4.8 <8.0" + "codeception/specify": "*", + "vlucas/phpdotenv": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Codeception\\PHPUnit\\": "src\\" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Davert", + "email": "davert.php@resend.cc" + } + ], + "description": "PHPUnit classes used by Codeception", + "time": "2019-02-26T20:47:56+00:00" + }, + { + "name": "codeception/stub", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Codeception/Stub.git", + "reference": "853657f988942f7afb69becf3fd0059f192c705a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/853657f988942f7afb69becf3fd0059f192c705a", + "reference": "853657f988942f7afb69becf3fd0059f192c705a", + "shasum": "" + }, + "require": { + "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3" }, "type": "library", "autoload": { @@ -4343,20 +4448,20 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-05-17T09:31:08+00:00" + "time": "2019-03-02T15:35:10+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.3.0", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "b8e9745fb9b06ea6664d8872c4505fb16df4611c" + "reference": "d17708133b6c276d6e42ef887a877866b909d892" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/b8e9745fb9b06ea6664d8872c4505fb16df4611c", - "reference": "b8e9745fb9b06ea6664d8872c4505fb16df4611c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/d17708133b6c276d6e42ef887a877866b909d892", + "reference": "d17708133b6c276d6e42ef887a877866b909d892", "shasum": "" }, "require": { @@ -4387,38 +4492,82 @@ "Xdebug", "performance" ], - "time": "2018-08-31T19:07:57+00:00" + "time": "2019-01-28T20:25:53+00:00" }, { "name": "consolidation/annotated-command", - "version": "2.9.1", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac" + "reference": "512a2e54c98f3af377589de76c43b24652bcb789" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac", - "reference": "4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/512a2e54c98f3af377589de76c43b24652bcb789", + "reference": "512a2e54c98f3af377589de76c43b24652bcb789", "shasum": "" }, "require": { - "consolidation/output-formatters": "^3.1.12", - "php": ">=5.4.0", + "consolidation/output-formatters": "^3.4", + "php": ">=5.4.5", "psr/log": "^1", "symfony/console": "^2.8|^3|^4", "symfony/event-dispatcher": "^2.5|^3|^4", "symfony/finder": "^2.5|^3|^4" }, "require-dev": { - "g1a/composer-test-scenarios": "^2", + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", "phpunit/phpunit": "^6", - "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.7" }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require": { + "symfony/console": "^4.0" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require": { + "symfony/console": "^2.8" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + }, + "scenario-options": { + "create-lockfile": "false" + } + }, + "phpunit4": { + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + } + }, "branch-alias": { "dev-master": "2.x-dev" } @@ -4439,20 +4588,20 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-09-19T17:47:18+00:00" + "time": "2019-03-08T16:55:03+00:00" }, { "name": "consolidation/config", - "version": "1.1.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/consolidation/config.git", - "reference": "c9fc25e9088a708637e18a256321addc0670e578" + "reference": "cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/config/zipball/c9fc25e9088a708637e18a256321addc0670e578", - "reference": "c9fc25e9088a708637e18a256321addc0670e578", + "url": "https://api.github.com/repos/consolidation/config/zipball/cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1", + "reference": "cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1", "shasum": "" }, "require": { @@ -4461,9 +4610,9 @@ "php": ">=5.4.0" }, "require-dev": { - "g1a/composer-test-scenarios": "^1", + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", "phpunit/phpunit": "^5", - "satooshi/php-coveralls": "^1.0", "squizlabs/php_codesniffer": "2.*", "symfony/console": "^2.5|^3|^4", "symfony/yaml": "^2.8.11|^3|^4" @@ -4473,6 +4622,33 @@ }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require-dev": { + "symfony/console": "^4.0" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require-dev": { + "symfony/console": "^2.8", + "symfony/event-dispatcher": "^2.8", + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + } + }, "branch-alias": { "dev-master": "1.x-dev" } @@ -4493,35 +4669,76 @@ } ], "description": "Provide configuration services for a commandline tool.", - "time": "2018-08-07T22:57:00+00:00" + "time": "2019-03-03T19:37:04+00:00" }, { "name": "consolidation/log", - "version": "1.0.6", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/consolidation/log.git", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395" + "reference": "b2e887325ee90abc96b0a8b7b474cd9e7c896e3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/log/zipball/dfd8189a771fe047bf3cd669111b2de5f1c79395", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395", + "url": "https://api.github.com/repos/consolidation/log/zipball/b2e887325ee90abc96b0a8b7b474cd9e7c896e3a", + "reference": "b2e887325ee90abc96b0a8b7b474cd9e7c896e3a", "shasum": "" }, "require": { - "php": ">=5.5.0", - "psr/log": "~1.0", + "php": ">=5.4.5", + "psr/log": "^1.0", "symfony/console": "^2.8|^3|^4" }, "require-dev": { - "g1a/composer-test-scenarios": "^1", - "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "2.*" + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", + "phpunit/phpunit": "^6", + "squizlabs/php_codesniffer": "^2" }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require": { + "symfony/console": "^4.0" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require": { + "symfony/console": "^2.8" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + }, + "phpunit4": { + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + } + }, "branch-alias": { "dev-master": "1.x-dev" } @@ -4542,33 +4759,33 @@ } ], "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.", - "time": "2018-05-25T18:14:39+00:00" + "time": "2019-01-01T17:30:51+00:00" }, { "name": "consolidation/output-formatters", - "version": "3.2.1", + "version": "3.4.1", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5" + "reference": "0881112642ad9059071f13f397f571035b527cb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/0881112642ad9059071f13f397f571035b527cb9", + "reference": "0881112642ad9059071f13f397f571035b527cb9", "shasum": "" }, "require": { + "dflydev/dot-access-data": "^1.1.0", "php": ">=5.4.0", "symfony/console": "^2.8|^3|^4", "symfony/finder": "^2.5|^3|^4" }, "require-dev": { - "g1a/composer-test-scenarios": "^2", + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", "phpunit/phpunit": "^5.7.27", - "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.7", - "symfony/console": "3.2.3", "symfony/var-dumper": "^2.8|^3|^4", "victorjonsson/markdowndocs": "^1.3" }, @@ -4577,6 +4794,52 @@ }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require": { + "symfony/console": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^6" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony3": { + "require": { + "symfony/console": "^3.4", + "symfony/finder": "^3.4", + "symfony/var-dumper": "^3.4" + }, + "config": { + "platform": { + "php": "5.6.32" + } + } + }, + "symfony2": { + "require": { + "symfony/console": "^2.8" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + }, + "scenario-options": { + "create-lockfile": "false" + } + } + }, "branch-alias": { "dev-master": "3.x-dev" } @@ -4597,29 +4860,28 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2018-05-25T18:02:34+00:00" + "time": "2019-03-14T03:45:44+00:00" }, { "name": "consolidation/robo", - "version": "1.3.1", + "version": "1.4.9", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d" + "reference": "5c6b3840a45afda1cbffbb3bb1f94dd5f9f83345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", - "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/5c6b3840a45afda1cbffbb3bb1f94dd5f9f83345", + "reference": "5c6b3840a45afda1cbffbb3bb1f94dd5f9f83345", "shasum": "" }, "require": { - "consolidation/annotated-command": "^2.8.2", - "consolidation/config": "^1.0.10", + "consolidation/annotated-command": "^2.10.2", + "consolidation/config": "^1.2", "consolidation/log": "~1", "consolidation/output-formatters": "^3.1.13", "consolidation/self-update": "^1", - "g1a/composer-test-scenarios": "^2", "grasmash/yaml-expander": "^1.3", "league/container": "^2.2", "php": ">=5.5.0", @@ -4636,14 +4898,15 @@ "codeception/aspect-mock": "^1|^2.1.1", "codeception/base": "^2.3.7", "codeception/verify": "^0.3.2", + "g1a/composer-test-scenarios": "^3", "goaop/framework": "~2.1.2", "goaop/parser-reflection": "^1.1.0", "natxet/cssmin": "3.0.4", "nikic/php-parser": "^3.1.5", "patchwork/jsqueeze": "~2", - "pear/archive_tar": "^1.4.2", + "pear/archive_tar": "^1.4.4", + "php-coveralls/php-coveralls": "^1", "phpunit/php-code-coverage": "~2|~4", - "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.8" }, "suggest": { @@ -4657,9 +4920,36 @@ ], "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require": { + "symfony/console": "^4" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require": { + "symfony/console": "^2.8" + }, + "remove": [ + "goaop/framework" + ], + "config": { + "platform": { + "php": "5.5.9" + } + }, + "scenario-options": { + "create-lockfile": "false" + } + } + }, "branch-alias": { - "dev-master": "1.x-dev", - "dev-state": "1.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -4678,20 +4968,20 @@ } ], "description": "Modern task runner", - "time": "2018-08-17T18:44:18+00:00" + "time": "2019-03-19T18:07:19+00:00" }, { "name": "consolidation/self-update", - "version": "1.1.3", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/consolidation/self-update.git", - "reference": "de33822f907e0beb0ffad24cf4b1b4fae5ada318" + "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/self-update/zipball/de33822f907e0beb0ffad24cf4b1b4fae5ada318", - "reference": "de33822f907e0beb0ffad24cf4b1b4fae5ada318", + "url": "https://api.github.com/repos/consolidation/self-update/zipball/a1c273b14ce334789825a09d06d4c87c0a02ad54", + "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54", "shasum": "" }, "require": { @@ -4728,7 +5018,7 @@ } ], "description": "Provides a self:update command for Symfony Console applications.", - "time": "2018-08-24T17:01:46+00:00" + "time": "2018-10-28T01:52:03+00:00" }, { "name": "dflydev/dot-access-data", @@ -5317,39 +5607,6 @@ ], "time": "2018-07-12T10:23:15+00:00" }, - { - "name": "g1a/composer-test-scenarios", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/g1a/composer-test-scenarios.git", - "reference": "a166fd15191aceab89f30c097e694b7cf3db4880" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/g1a/composer-test-scenarios/zipball/a166fd15191aceab89f30c097e694b7cf3db4880", - "reference": "a166fd15191aceab89f30c097e694b7cf3db4880", - "shasum": "" - }, - "bin": [ - "scripts/create-scenario", - "scripts/dependency-licenses", - "scripts/install-scenario" - ], - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Useful scripts for testing multiple sets of Composer dependencies.", - "time": "2018-08-08T23:37:23+00:00" - }, { "name": "grasmash/expander", "version": "1.0.0", @@ -5563,32 +5820,33 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.4.2", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + "reference": "9f83dded91781a01c63574e387eaa769be769115" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", "shasum": "" }, "require": { "php": ">=5.4.0", - "psr/http-message": "~1.0" + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" }, "provide": { "psr/http-message-implementation": "1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -5618,13 +5876,14 @@ "keywords": [ "http", "message", + "psr-7", "request", "response", "stream", "uri", "url" ], - "time": "2017-03-20T17:10:46+00:00" + "time": "2018-12-04T20:46:45+00:00" }, { "name": "ircmaxell/password-compat", @@ -5670,16 +5929,16 @@ }, { "name": "jms/metadata", - "version": "1.6.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/metadata.git", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab" + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/6a06970a10e0a532fb52d3959547123b84a3b3ab", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", "shasum": "" }, "require": { @@ -5702,9 +5961,13 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, { "name": "Johannes M. Schmitt", "email": "schmittjoh@gmail.com" @@ -5717,7 +5980,7 @@ "xml", "yaml" ], - "time": "2016-12-05T10:18:33+00:00" + "time": "2018-10-26T12:40:10+00:00" }, { "name": "jms/parser-lib", @@ -7094,8 +7357,49 @@ "mock", "xunit" ], + "abandoned": true, "time": "2017-08-03T14:08:16+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" + }, { "name": "sebastian/code-unit-reverse-lookup", "version": "1.0.1", @@ -7798,16 +8102,16 @@ }, { "name": "symfony/browser-kit", - "version": "v3.4.17", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "f6668d1a6182d5a8dec65a1c863a4c1d963816c0" + "reference": "c0fadd368c1031109e996316e53ffeb886d37ea1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/f6668d1a6182d5a8dec65a1c863a4c1d963816c0", - "reference": "f6668d1a6182d5a8dec65a1c863a4c1d963816c0", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/c0fadd368c1031109e996316e53ffeb886d37ea1", + "reference": "c0fadd368c1031109e996316e53ffeb886d37ea1", "shasum": "" }, "require": { @@ -7851,20 +8155,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2018-07-26T09:06:28+00:00" + "time": "2019-02-23T15:06:07+00:00" }, { "name": "symfony/config", - "version": "v3.4.17", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "e5389132dc6320682de3643091121c048ff796b3" + "reference": "177a276c01575253c95cefe0866e3d1b57637fe0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/e5389132dc6320682de3643091121c048ff796b3", - "reference": "e5389132dc6320682de3643091121c048ff796b3", + "url": "https://api.github.com/repos/symfony/config/zipball/177a276c01575253c95cefe0866e3d1b57637fe0", + "reference": "177a276c01575253c95cefe0866e3d1b57637fe0", "shasum": "" }, "require": { @@ -7915,60 +8219,7 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-09-08T13:15:14+00:00" - }, - { - "name": "symfony/css-selector", - "version": "v3.4.17", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "3503415d4aafabc31cd08c3a4ebac7f43fde8feb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/3503415d4aafabc31cd08c3a4ebac7f43fde8feb", - "reference": "3503415d4aafabc31cd08c3a4ebac7f43fde8feb", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony CssSelector Component", - "homepage": "https://symfony.com", - "time": "2018-10-02T16:33:53+00:00" + "time": "2019-02-23T15:06:07+00:00" }, { "name": "symfony/dependency-injection", @@ -8042,16 +8293,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v3.4.17", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "c705bee03ade5b47c087807dd9ffaaec8dda2722" + "reference": "d40023c057393fb25f7ca80af2a56ed948c45a09" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c705bee03ade5b47c087807dd9ffaaec8dda2722", - "reference": "c705bee03ade5b47c087807dd9ffaaec8dda2722", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d40023c057393fb25f7ca80af2a56ed948c45a09", + "reference": "d40023c057393fb25f7ca80af2a56ed948c45a09", "shasum": "" }, "require": { @@ -8095,20 +8346,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2018-10-02T12:28:39+00:00" + "time": "2019-02-23T15:06:07+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.4.17", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1" + "reference": "9a96d77ceb1fd913c9d4a89e8a7e1be87604be8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3a4498236ade473c52b92d509303e5fd1b211ab1", - "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9a96d77ceb1fd913c9d4a89e8a7e1be87604be8a", + "reference": "9a96d77ceb1fd913c9d4a89e8a7e1be87604be8a", "shasum": "" }, "require": { @@ -8149,20 +8400,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-10-03T08:48:18+00:00" + "time": "2019-02-23T15:06:07+00:00" }, { "name": "symfony/options-resolver", - "version": "v3.4.17", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "1cf7d8e704a9cc4164c92e430f2dfa3e6983661d" + "reference": "926e3b797e6bb66c0e4d7da7eff3a174f7378bcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/1cf7d8e704a9cc4164c92e430f2dfa3e6983661d", - "reference": "1cf7d8e704a9cc4164c92e430f2dfa3e6983661d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/926e3b797e6bb66c0e4d7da7eff3a174f7378bcf", + "reference": "926e3b797e6bb66c0e4d7da7eff3a174f7378bcf", "shasum": "" }, "require": { @@ -8203,20 +8454,20 @@ "configuration", "options" ], - "time": "2018-09-17T17:29:18+00:00" + "time": "2019-02-23T15:06:07+00:00" }, { "name": "symfony/polyfill-php54", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php54.git", - "reference": "412977e090c6a8472dc39d50d1beb7d59495a965" + "reference": "2964b17ddc32dba7bcba009d5501c84d3fba1452" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/412977e090c6a8472dc39d50d1beb7d59495a965", - "reference": "412977e090c6a8472dc39d50d1beb7d59495a965", + "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/2964b17ddc32dba7bcba009d5501c84d3fba1452", + "reference": "2964b17ddc32dba7bcba009d5501c84d3fba1452", "shasum": "" }, "require": { @@ -8225,7 +8476,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -8261,20 +8512,20 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/polyfill-php55", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php55.git", - "reference": "578b8528da843de0fc65ec395900fa3181f2ead7" + "reference": "96fa25cef405ea452919559a0025d5dc16e30e4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/578b8528da843de0fc65ec395900fa3181f2ead7", - "reference": "578b8528da843de0fc65ec395900fa3181f2ead7", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/96fa25cef405ea452919559a0025d5dc16e30e4c", + "reference": "96fa25cef405ea452919559a0025d5dc16e30e4c", "shasum": "" }, "require": { @@ -8284,7 +8535,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -8317,20 +8568,20 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "1e24b0c4a56d55aaf368763a06c6d1c7d3194934" + "reference": "bc4858fb611bda58719124ca079baff854149c89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/1e24b0c4a56d55aaf368763a06c6d1c7d3194934", - "reference": "1e24b0c4a56d55aaf368763a06c6d1c7d3194934", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/bc4858fb611bda58719124ca079baff854149c89", + "reference": "bc4858fb611bda58719124ca079baff854149c89", "shasum": "" }, "require": { @@ -8340,7 +8591,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -8376,20 +8627,20 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "95c50420b0baed23852452a7f0c7b527303ed5ae" + "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/95c50420b0baed23852452a7f0c7b527303ed5ae", - "reference": "95c50420b0baed23852452a7f0c7b527303ed5ae", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c", + "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c", "shasum": "" }, "require": { @@ -8398,7 +8649,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -8431,20 +8682,20 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/stopwatch", - "version": "v3.4.17", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "05e52a39de52ba690aebaed462b2bc8a9649f0a4" + "reference": "2a651c2645c10bbedd21170771f122d935e0dd58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/05e52a39de52ba690aebaed462b2bc8a9649f0a4", - "reference": "05e52a39de52ba690aebaed462b2bc8a9649f0a4", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/2a651c2645c10bbedd21170771f122d935e0dd58", + "reference": "2a651c2645c10bbedd21170771f122d935e0dd58", "shasum": "" }, "require": { @@ -8480,7 +8731,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2018-10-02T12:28:39+00:00" + "time": "2019-01-16T09:39:14+00:00" }, { "name": "symfony/yaml", @@ -8619,20 +8870,21 @@ }, { "name": "vlucas/phpdotenv", - "version": "v2.5.1", + "version": "v2.6.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e" + "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", - "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2a7dcf7e3e02dc5e701004e51a6f304b713107d5", + "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/polyfill-ctype": "^1.9" }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.0" @@ -8640,7 +8892,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -8665,24 +8917,25 @@ "env", "environment" ], - "time": "2018-07-29T20:33:41+00:00" + "time": "2019-01-29T11:11:52+00:00" }, { "name": "webmozart/assert", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "phpunit/phpunit": "^4.6", @@ -8715,7 +8968,7 @@ "check", "validate" ], - "time": "2018-01-29T19:49:41+00:00" + "time": "2018-12-25T11:19:39+00:00" } ], "aliases": [], diff --git a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php index 9682c4b2ee603..39a64c94067aa 100644 --- a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php @@ -472,6 +472,10 @@ public function testUpdateDownloadableProductSamplesWithNewFile() 'title' => 'sample2_updated', 'sort_order' => 2, 'sample_type' => 'file', + 'sample_file_content' => [ + 'name' => 'sample2.jpg', + 'file_data' => base64_encode(file_get_contents($this->testImagePath)), + ], ]; $response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["downloadable_product_samples"] = diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php index 6dbf2b1aa6a12..9edd087020a72 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php @@ -195,6 +195,13 @@ class ConditionsElement extends SimpleElement */ protected $exception; + /** + * Condition option text selector. + * + * @var string + */ + private $conditionOptionTextSelector = '//option[normalize-space(text())="%s"]'; + /** * @inheritdoc */ @@ -282,10 +289,16 @@ protected function addCondition($type, ElementInterface $context) $count = 0; do { - $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); - try { - $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select')->setValue($type); + $specificType = $newCondition->find( + sprintf($this->conditionOptionTextSelector, $type), + Locator::SELECTOR_XPATH + )->isPresent(); + $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); + $condition = $specificType + ? $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'selectcondition') + : $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select'); + $condition->setValue($type); $isSetType = true; } catch (\PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) { $isSetType = false; diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php new file mode 100644 index 0000000000000..15a799eac5188 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Mtf\Client\Element; + +/** + * @inheritdoc + */ +class SelectconditionElement extends SelectElement +{ + /** + * @inheritdoc + */ + protected $optionByValue = './/option[normalize-space(.)=%s]'; +} diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php b/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php index 2fc26a975e159..a5e09232b78a4 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php @@ -81,7 +81,9 @@ protected function authorize() $urls[] = $url2; if (strpos($originalUrl, 'https') !== false) { $urls[] = str_replace('https', 'http', $originalUrl); + $urls[] = str_replace('https', 'http', $url2); } else { + $urls[] = str_replace('http', 'https', $originalUrl); $urls[] = str_replace('http', 'https', $url2); } diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Block/System/Store/StoreGrid.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Block/System/Store/StoreGrid.php index 87a3ed048dbb9..275b1225f71a7 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Block/System/Store/StoreGrid.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Block/System/Store/StoreGrid.php @@ -6,30 +6,37 @@ namespace Magento\Backend\Test\Block\System\Store; -use Magento\Backend\Test\Block\Widget\Grid; +use Magento\Mtf\Client\Locator; use Magento\Store\Test\Fixture\Store; use Magento\Store\Test\Fixture\StoreGroup; use Magento\Store\Test\Fixture\Website; -use Magento\Mtf\Client\Locator; +use Magento\Ui\Test\Block\Adminhtml\DataGrid; /** * Adminhtml Store View management grid. */ -class StoreGrid extends Grid +class StoreGrid extends DataGrid { /** - * Locator value for opening needed row. + * Secondary part of row locator template for getRow() method * * @var string */ - protected $editLink = 'td[data-column="store_title"] > a'; + protected $rowTemplate = 'td[div[*[contains(.,normalize-space("%s"))]]]'; /** - * Secondary part of row locator template for getRow() method with strict option. + * Secondary part of row locator template for getRow() method with strict option + * + * @var string + */ + protected $rowTemplateStrict = 'td[div[*[text()[normalize-space()="%s"]]]]'; + + /** + * Locator value for opening needed row. * * @var string */ - protected $rowTemplateStrict = '//*[text()[normalize-space()="%s"]]'; + protected $editLink = '[href*="editStore"]'; /** * Filters array mapping. @@ -38,13 +45,13 @@ class StoreGrid extends Grid */ protected $filters = [ 'store_title' => [ - 'selector' => '#storeGrid_filter_store_title', + 'selector' => '[name="store_title"]', ], 'group_title' => [ - 'selector' => '#storeGrid_filter_group_title', + 'selector' => '[name="group_title"]', ], 'website_title' => [ - 'selector' => '#storeGrid_filter_website_title', + 'selector' => '[name="name"]', ], ]; @@ -53,7 +60,7 @@ class StoreGrid extends Grid * * @var string */ - protected $titleFormat = '//td[a[.="%s"]]'; + protected $titleFormat = '//a[.="%s"]'; /** * Store name link selector. diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/StoreIndex.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/StoreIndex.xml index 0491106412fe5..bc2b91fe65e6b 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/StoreIndex.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/StoreIndex.xml @@ -9,6 +9,6 @@ <page name="StoreIndex" area="Adminhtml" mca="admin/system_store" module="Magento_Backend"> <block name="messagesBlock" class="Magento\Backend\Test\Block\Messages" locator="#messages" strategy="css selector"/> <block name="gridPageActions" class="Magento\Backend\Test\Block\System\Store\GridPageActions" locator=".page-main-actions" strategy="css selector"/> - <block name="storeGrid" class="Magento\Backend\Test\Block\System\Store\StoreGrid" locator="[id='page:main-container']" strategy="css selector"/> + <block name="storeGrid" class="Magento\Backend\Test\Block\System\Store\StoreGrid" locator="//div[contains(@data-bind, 'store_listing')]" strategy="xpath"/> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php b/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php index 6be21d2e71f9f..3653b0b2b5857 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php @@ -6,7 +6,7 @@ namespace Magento\Captcha\Test\Constraint; -use Magento\Contact\Test\Page\ContactIndex; +use Magento\Captcha\Test\Page\ContactIndexCaptcha as ContactIndex; use Magento\Mtf\Constraint\AbstractConstraint; /** diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml index 060fc5f346fda..742eabb61f371 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> - <page name="ContactIndex" mca="contact/index/index" module="Magento_Contact"> + <page name="ContactIndexCaptcha" mca="contact/index/index" module="Magento_Captcha"> <block name="contactUs" class="Magento\Captcha\Test\Block\Form\ContactUs" locator="#contact-form" strategy="css selector" /> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php index d8c9bf1f719de..0de71c3a416c8 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php @@ -8,7 +8,7 @@ use Magento\Captcha\Test\Constraint\AssertCaptchaFieldOnContactUsForm; use Magento\Contact\Test\Fixture\Comment; -use Magento\Contact\Test\Page\ContactIndex; +use Magento\Captcha\Test\Page\ContactIndexCaptcha as ContactIndex; use Magento\Mtf\TestCase\Injectable; use Magento\Mtf\TestStep\TestStepFactory; diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php index ccb0d43337562..9f35059556fa8 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php @@ -128,7 +128,8 @@ class CustomOptions extends Form * * @var string */ - private $validationErrorMessage = '//div[@class="mage-error"][contains(text(), "required field")]'; + private $validationErrorMessage = '//div[@class="mage-error"][contains(text(), "required field")' . + ' and not(contains(@style,"display"))]'; /** * Get product options diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml index 3c88d9193db28..f41731287a5ec 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml @@ -14,7 +14,6 @@ <data name="editShippingAddress/dataset" xsi:type="string">empty_UK_address_without_email</data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> - <data name="billingCheckboxState" xsi:type="string">Yes</data> <data name="products" xsi:type="array"> <item name="0" xsi:type="string">catalogProductSimple::default</item> </data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml index 9e20bbdaac1d9..6716e4aacab81 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml @@ -15,13 +15,11 @@ <data name="shippingAddressCustomer" xsi:type="array"> <item name="added" xsi:type="number">1</item> </data> - <data name="billingAddressCustomer" xsi:type="array"> - <item name="added" xsi:type="number">1</item> - </data> <data name="prices" xsi:type="array"> <item name="grandTotal" xsi:type="string">565.00</item> </data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="editBillingInformation" xsi:type="boolean">false</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> <data name="payment/method" xsi:type="string">checkmo</data> <data name="configData" xsi:type="string">checkmo</data> @@ -43,6 +41,7 @@ <item name="grandTotal" xsi:type="string">565.00</item> </data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="editBillingInformation" xsi:type="boolean">false</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> <data name="payment/method" xsi:type="string">checkmo</data> <data name="configData" xsi:type="string">checkmo</data> @@ -61,7 +60,7 @@ <data name="shippingAddress/dataset" xsi:type="string">UK_address_without_email</data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> - <data name="billingCheckboxState" xsi:type="string">Yes</data> + <data name="editBillingInformation" xsi:type="boolean">false</data> <data name="billingAddress/dataset" xsi:type="string">US_address_1_without_email</data> <data name="payment/method" xsi:type="string">checkmo</data> <data name="configData" xsi:type="string">checkmo</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php index aa7eba634145f..f13fb93e89b71 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php @@ -126,11 +126,15 @@ public function run() if ($this->billingCheckboxState) { $this->assertBillingAddressCheckbox->processAssert($this->checkoutOnepage, $this->billingCheckboxState); } - if ($this->billingCheckboxState === 'Yes' && !$this->editBillingInformation) { - return [ - 'billingAddress' => $this->shippingAddress - ]; + + if (!$this->editBillingInformation) { + $billingAddress = $this->billingCheckboxState === 'Yes' + ? $this->shippingAddress + : $this->getDefaultBillingAddress(); + + return ['billingAddress' => $billingAddress]; } + if ($this->billingAddress) { $selectedPaymentMethod = $this->checkoutOnepage->getPaymentBlock()->getSelectedPaymentMethodBlock(); if ($this->shippingAddress) { @@ -156,4 +160,25 @@ public function run() 'billingAddress' => $billingAddress ]; } + + /** + * Get default billing address + * + * @return Address|null + */ + private function getDefaultBillingAddress() + { + $addresses = $this->customer->hasData('address') + ? $this->customer->getDataFieldConfig('address')['source']->getAddresses() + : []; + $defaultAddress = null; + foreach ($addresses as $address) { + if ($address->getDefaultBilling() === 'Yes') { + $defaultAddress = $address; + break; + } + } + + return $defaultAddress; + } } diff --git a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml index d304d305a7265..a266b09278ddb 100644 --- a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml +++ b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> <page name="MultishippingCheckoutOverview" mca="multishipping/checkout/overview" module="Magento_Checkout"> - <block name="agreementReview" class="Magento\CheckoutAgreements\Test\Block\Multishipping\MultishippingAgreementReview" locator="#checkout-agreements" strategy="css selector"/> + <block name="agreementReview" class="Magento\CheckoutAgreements\Test\Block\Multishipping\MultishippingAgreementReview" locator=".checkout-agreements" strategy="css selector"/> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Config/Test/Handler/ConfigData/Curl.php b/dev/tests/functional/tests/app/Magento/Config/Test/Handler/ConfigData/Curl.php index 66587879848a3..0d89a1d4eba6e 100644 --- a/dev/tests/functional/tests/app/Magento/Config/Test/Handler/ConfigData/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Config/Test/Handler/ConfigData/Curl.php @@ -123,9 +123,9 @@ protected function prepareConfigPath(array $input) */ protected function applyConfigSettings(array $data, $section) { - $url = $this->getUrl($section); $curl = new BackendDecorator(new CurlTransport(), $this->_configuration); $curl->addOption(CURLOPT_HEADER, 1); + $url = $this->getUrl($section); $curl->write($url, $data); $response = $curl->read(); $curl->close(); diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Store/Curl.php b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Store/Curl.php index 2fb05e59c379a..091cea329b78e 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Store/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Store/Curl.php @@ -90,22 +90,29 @@ protected function prepareData(FixtureInterface $fixture) */ protected function getStoreId($name) { - //Set pager limit to 2000 in order to find created store view by name - $url = $_ENV['app_backend_url'] . 'admin/system_store/index/sort/store_title/dir/asc/limit/2000'; + $url = $_ENV['app_backend_url'] . 'mui/index/render/'; + $data = [ + 'namespace' => 'store_listing', + 'filters' => [ + 'placeholder' => true, + 'store_title' => $name, + ], + 'paging' => [ + 'pageSize' => 1, + ] + ]; $curl = new BackendDecorator(new CurlTransport(), $this->_configuration); - $curl->addOption(CURLOPT_HEADER, 1); - $curl->write($url, [], CurlInterface::GET); + + $curl->write($url, $data, CurlInterface::POST); $response = $curl->read(); + $curl->close(); - $expectedUrl = '/admin/system_store/editStore/store_id/'; - $expectedUrl = preg_quote($expectedUrl); - $expectedUrl = str_replace('/', '\/', $expectedUrl); - preg_match('/' . $expectedUrl . '([0-9]*)\/(.)*>' . $name . '<\/a>/', $response, $matches); + preg_match('/store_listing_data_source.+items.+"store_id":"(\d+)"/', $response, $match); - if (empty($matches)) { + if (empty($match)) { throw new \Exception('Cannot find store id'); } - return empty($matches[1]) ? null : $matches[1]; + return intval($match[1]); } } diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/StoreGroup/Curl.php b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/StoreGroup/Curl.php index d593165405102..511f0ed4f4a44 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/StoreGroup/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/StoreGroup/Curl.php @@ -49,23 +49,30 @@ public function persist(FixtureInterface $fixture = null) */ protected function getStoreGroupIdByGroupName($storeName) { - //Set pager limit to 2000 in order to find created store group by name - $url = $_ENV['app_backend_url'] . 'admin/system_store/index/sort/group_title/dir/asc/limit/2000'; + $url = $_ENV['app_backend_url'] . 'mui/index/render/'; + $data = [ + 'namespace' => 'store_listing', + 'filters' => [ + 'placeholder' => true, + 'group_title' => $storeName, + ], + 'paging' => [ + 'pageSize' => 1, + ] + ]; $curl = new BackendDecorator(new CurlTransport(), $this->_configuration); - $curl->addOption(CURLOPT_HEADER, 1); - $curl->write($url, [], CurlInterface::GET); + + $curl->write($url, $data, CurlInterface::POST); $response = $curl->read(); + $curl->close(); - $expectedUrl = '/admin/system_store/editGroup/group_id/'; - $expectedUrl = preg_quote($expectedUrl); - $expectedUrl = str_replace('/', '\/', $expectedUrl); - preg_match('/' . $expectedUrl . '([0-9]*)\/(.)*>' . $storeName . '<\/a>/', $response, $matches); + preg_match('/store_listing_data_source.+items.+"group_id":"(\d+)"/', $response, $match); - if (empty($matches)) { + if (empty($match)) { throw new \Exception('Cannot find store group id'); } - return (int)$matches[1]; + return (int)$match[1]; } /** diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php index ecc59ebb55ca5..0e83e36c0f109 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php @@ -103,23 +103,30 @@ public function persist(FixtureInterface $fixture = null) */ protected function getWebSiteIdByWebsiteName($websiteName) { - // Set pager limit to 2000 in order to find created website by name - $url = $_ENV['app_backend_url'] . 'admin/system_store/index/sort/group_title/dir/asc/limit/2000'; + $url = $_ENV['app_backend_url'] . 'mui/index/render/'; + $data = [ + 'namespace' => 'store_listing', + 'filters' => [ + 'placeholder' => true, + 'name' => $websiteName, + ], + 'paging' => [ + 'pageSize' => 1, + ] + ]; $curl = new BackendDecorator(new CurlTransport(), $this->_configuration); - $curl->addOption(CURLOPT_HEADER, 1); - $curl->write($url, [], CurlInterface::GET); + + $curl->write($url, $data, CurlInterface::POST); $response = $curl->read(); + $curl->close(); - $expectedUrl = '/admin/system_store/editWebsite/website_id/'; - $expectedUrl = preg_quote($expectedUrl); - $expectedUrl = str_replace('/', '\/', $expectedUrl); - preg_match('/' . $expectedUrl . '([0-9]*)\/(.)*>' . $websiteName . '<\/a>/', $response, $matches); + preg_match('/store_listing_data_source.+items.+"website_id":"(\d+)"/', $response, $match); - if (empty($matches)) { + if (empty($match)) { throw new \Exception('Cannot find website id.'); } - return (int)$matches[1]; + return (int)$match[1]; } /** diff --git a/dev/tests/integration/etc/di/preferences/ce.php b/dev/tests/integration/etc/di/preferences/ce.php index 6839d0ae9a7ff..fe2c1c7d01740 100644 --- a/dev/tests/integration/etc/di/preferences/ce.php +++ b/dev/tests/integration/etc/di/preferences/ce.php @@ -29,4 +29,5 @@ \Magento\TestFramework\Lock\Backend\DummyLocker::class, \Magento\Framework\ShellInterface::class => \Magento\TestFramework\App\Shell::class, \Magento\Framework\App\Shell::class => \Magento\TestFramework\App\Shell::class, + \Magento\Framework\Session\SessionStartChecker::class => \Magento\TestFramework\Session\SessionStartChecker::class, ]; diff --git a/dev/tests/integration/framework/Magento/TestFramework/Isolation/DeploymentConfig.php b/dev/tests/integration/framework/Magento/TestFramework/Isolation/DeploymentConfig.php index 3132aed4d21e3..ae59bc004db0e 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Isolation/DeploymentConfig.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Isolation/DeploymentConfig.php @@ -51,7 +51,7 @@ public function startTestSuite() { if (null === $this->reader) { $this->reader = Bootstrap::getObjectManager()->get(\Magento\Framework\App\DeploymentConfig\Reader::class); - $this->config = $this->reader->load(); + $this->config = $this->filterIgnoredConfigValues($this->reader->load()); } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Session/SessionStartChecker.php b/dev/tests/integration/framework/Magento/TestFramework/Session/SessionStartChecker.php new file mode 100644 index 0000000000000..136b0565a729a --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Session/SessionStartChecker.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestFramework\Session; + +/** + * Class to check if session can be started or not. Dummy for integration tests. + */ +class SessionStartChecker extends \Magento\Framework\Session\SessionStartChecker +{ + /** + * Can session be started or not. + * + * @return bool + */ + public function check() : bool + { + return true; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php index 0845ef640aa0c..efa7cda029994 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php @@ -5,10 +5,12 @@ */ namespace Magento\TestFramework\TestCase; +use Magento\Framework\App\Request\Http as HttpRequest; + /** - * A parent class for backend controllers - contains directives for admin user creation and authentication + * A parent class for backend controllers - contains directives for admin user creation and authentication. + * * @SuppressWarnings(PHPMD.NumberOfChildren) - * @SuppressWarnings(PHPMD.numberOfChildren) */ abstract class AbstractBackendController extends \Magento\TestFramework\TestCase\AbstractController { @@ -36,6 +38,16 @@ abstract class AbstractBackendController extends \Magento\TestFramework\TestCase */ protected $uri = null; + /** + * @var string|null + */ + protected $httpMethod; + + /** + * @inheritDoc + * + * @throws \Magento\Framework\Exception\AuthenticationException + */ protected function setUp() { parent::setUp(); @@ -62,6 +74,9 @@ protected function _getAdminCredentials() ]; } + /** + * @inheritDoc + */ protected function tearDown() { $this->_auth->getAuthStorage()->destroy(['send_expire_cookie' => false]); @@ -86,21 +101,33 @@ public function assertSessionMessages( parent::assertSessionMessages($constraint, $messageType, $messageManagerClass); } + /** + * Test ACL configuration for action working. + */ public function testAclHasAccess() { if ($this->uri === null) { $this->markTestIncomplete('AclHasAccess test is not complete'); } + if ($this->httpMethod) { + $this->getRequest()->setMethod($this->httpMethod); + } $this->dispatch($this->uri); $this->assertNotSame(403, $this->getResponse()->getHttpResponseCode()); $this->assertNotSame(404, $this->getResponse()->getHttpResponseCode()); } + /** + * Test ACL actually denying access. + */ public function testAclNoAccess() { if ($this->resource === null || $this->uri === null) { $this->markTestIncomplete('Acl test is not complete'); } + if ($this->httpMethod) { + $this->getRequest()->setMethod($this->httpMethod); + } $this->_objectManager->get(\Magento\Framework\Acl\Builder::class) ->getAcl() ->deny(null, $this->resource); diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractController.php index 9920f90193f69..feb9eca0793a2 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractController.php @@ -9,9 +9,13 @@ */ namespace Magento\TestFramework\TestCase; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; use Magento\Framework\Stdlib\CookieManagerInterface; use Magento\Framework\View\Element\Message\InterpretationStrategyInterface; use Magento\Theme\Controller\Result\MessagePlugin; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\App\Response\Http as HttpResponse; /** * @SuppressWarnings(PHPMD.NumberOfChildren) @@ -68,6 +72,9 @@ protected function setUp() $this->_objectManager->removeSharedInstance(\Magento\Framework\App\RequestInterface::class); } + /** + * @inheritDoc + */ protected function tearDown() { $this->_request = null; @@ -96,14 +103,23 @@ protected function assertPostConditions() */ public function dispatch($uri) { - $this->getRequest()->setRequestUri($uri); + /** @var HttpRequest $request */ + $request = $this->getRequest(); + $request->setRequestUri($uri); + if ($request->isPost() + && !array_key_exists('form_key', $request->getPost()) + ) { + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $request->setPostValue('form_key', $formKey->getFormKey()); + } $this->_getBootstrap()->runApp(); } /** * Request getter * - * @return \Magento\Framework\App\RequestInterface + * @return \Magento\Framework\App\RequestInterface|HttpRequest */ public function getRequest() { @@ -116,7 +132,7 @@ public function getRequest() /** * Response getter * - * @return \Magento\Framework\App\ResponseInterface + * @return \Magento\Framework\App\ResponseInterface|HttpResponse */ public function getResponse() { @@ -201,13 +217,21 @@ public function assertSessionMessages( $messageManagerClass = \Magento\Framework\Message\Manager::class ) { $this->_assertSessionErrors = false; - + /** @var MessageInterface[]|string[] $messageObjects */ $messages = $this->getMessages($messageType, $messageManagerClass); + /** @var string[] $messages */ + $messagesFiltered = array_map( + function ($message) { + /** @var MessageInterface|string $message */ + return ($message instanceof MessageInterface) ? $message->toString() : $message; + }, + $messages + ); $this->assertThat( - $messages, + $messagesFiltered, $constraint, - 'Session messages do not meet expectations ' . var_export($messages, true) + 'Session messages do not meet expectations ' . var_export($messagesFiltered, true) ); } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php index 17863cd709580..3c8930fb78097 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php @@ -15,6 +15,9 @@ class GraphTest extends \PHPUnit\Framework\TestCase */ protected $_block; + /** + * @inheritdoc + */ protected function setUp() { parent::setUp(); @@ -25,8 +28,13 @@ protected function setUp() $this->_block->setDataHelper($objectManager->get(\Magento\Backend\Helper\Dashboard\Order::class)); } + /** + * Tests getChartUrl. + * + * @return void + */ public function testGetChartUrl() { - $this->assertStringStartsWith('http://chart.apis.google.com/chart', $this->_block->getChartUrl()); + $this->assertStringStartsWith('https://image-charts.com/chart', $this->_block->getChartUrl()); } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php index 8aeee9cf12494..e11c5ce5d9cf3 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php @@ -87,41 +87,6 @@ public function testMassactionDefaultValues() $this->assertFalse($blockEmpty->isAvailable()); } - public function testGetJavaScript() - { - $this->loadLayout(); - - $javascript = $this->_block->getJavaScript(); - - $expectedItemFirst = '#"option_id1":{"label":"Option One",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"complete":"Test","id":"option_id1"}#'; - $this->assertRegExp($expectedItemFirst, $javascript); - - $expectedItemSecond = '#"option_id2":{"label":"Option Two",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"confirm":"Are you sure\?","id":"option_id2"}#'; - $this->assertRegExp($expectedItemSecond, $javascript); - } - - public function testGetJavaScriptWithAddedItem() - { - $this->loadLayout(); - - $input = [ - 'id' => 'option_id3', - 'label' => 'Option Three', - 'url' => '*/*/option3', - 'block_name' => 'admin.test.grid.massaction.option3', - ]; - $expected = '#"option_id3":{"id":"option_id3","label":"Option Three",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"block_name":"admin.test.grid.massaction.option3"}#'; - - $this->_block->addItem($input['id'], $input); - $this->assertRegExp($expected, $this->_block->getJavaScript()); - } - /** * @param string $mageMode * @param int $expectedCount @@ -213,21 +178,4 @@ public function getItemsDataProvider() ] ]; } - - public function testGridContainsMassactionColumn() - { - $this->loadLayout(); - $this->_layout->getBlock('admin.test.grid')->toHtml(); - - $gridMassactionColumn = $this->_layout->getBlock('admin.test.grid') - ->getColumnSet() - ->getChildBlock('massaction'); - - $this->assertNotNull($gridMassactionColumn, 'Massaction column does not exist in the grid column set'); - $this->assertInstanceOf( - \Magento\Backend\Block\Widget\Grid\Column::class, - $gridMassactionColumn, - 'Massaction column is not an instance of \Magento\Backend\Block\Widget\Column' - ); - } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php index 07af21505f180..4373523350c49 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php @@ -19,8 +19,15 @@ public function testAjaxBlockAction() $this->assertContains('dashboard-diagram', $actual); } + /** + * Tests tunnelAction. + * + * @return void + * @throws \Exception + */ public function testTunnelAction() { + // phpcs:disable Magento2.Functions.DiscouragedFunction $testUrl = \Magento\Backend\Block\Dashboard\Graph::API_URL . '?cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'; $handle = curl_init(); curl_setopt($handle, CURLOPT_URL, $testUrl); @@ -34,6 +41,7 @@ public function testTunnelAction() curl_close($handle); throw $e; } + // phpcs:enable $gaData = [ 'cht' => 'lc', diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/IndexTest.php index 219fde6e37075..d5a48b960811e 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/IndexTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Backend\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * @magentoAppArea adminhtml * @magentoDbIsolation enabled @@ -45,6 +47,7 @@ public function testLoggedIndexAction() public function testGlobalSearchAction() { $this->getRequest()->setParam('isAjax', 'true'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue('query', 'dummy'); $this->dispatch('backend/admin/index/globalSearch'); diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/UrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/UrlRewriteTest.php index 1185ae9727e98..0d48fc8b0f59c 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/UrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/UrlRewriteTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Backend\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * @magentoAppArea adminhtml */ @@ -20,6 +22,7 @@ public function testSaveActionCmsPage() $page = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Cms\Model\Page::class); $page->load('page_design_blank', 'identifier'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'description' => 'Some URL rewrite description', diff --git a/dev/tests/integration/testsuite/Magento/Backend/_files/allowed_countries_fr.php b/dev/tests/integration/testsuite/Magento/Backend/_files/allowed_countries_fr.php new file mode 100644 index 0000000000000..4bce8d95dafa6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/_files/allowed_countries_fr.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +use Magento\Framework\App\Config\ConfigResource\ConfigInterface; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ConfigInterface $config */ +$config = $objectManager->get(ConfigInterface::class); +$config->saveConfig('general/country/allow', 'FR'); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); diff --git a/dev/tests/integration/testsuite/Magento/Backend/_files/allowed_countries_fr_rollback.php b/dev/tests/integration/testsuite/Magento/Backend/_files/allowed_countries_fr_rollback.php new file mode 100644 index 0000000000000..711d985786329 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/_files/allowed_countries_fr_rollback.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +use Magento\Framework\App\Config\ConfigResource\ConfigInterface; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ConfigInterface $config */ +$config = $objectManager->get(ConfigInterface::class); +$config->deleteConfig('general/country/allow'); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php new file mode 100644 index 0000000000000..91cea7dc96602 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; + +$store = $storeManager->getStore(); +$quote->setReservedOrderId('multishipping_quote_id_braintree') + ->setStoreId($store->getId()) + ->setCustomerEmail('customer001@test.com'); + +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +$quote->collectTotals(); +$quoteRepository->save($quote); + +$items = $quote->getAllItems(); +$addressList = $quote->getAllShippingAddresses(); + +foreach ($addressList as $key => $address) { + $item = $items[$key]; + // set correct quantity per shipping address + $item->setQty(1); + $address->setTotalQty(1); + $address->addItem($item); +} + +// assign virtual product to the billing address +$billingAddress = $quote->getBillingAddress(); +$virtualItem = $items[sizeof($items) - 1]; +$billingAddress->setTotalQty(1); +$billingAddress->addItem($virtualItem); + +// need to recollect totals +$quote->setTotalsCollectedFlag(false); +$quote->collectTotals(); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree.php new file mode 100644 index 0000000000000..3e1db90f1f2c8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote\Payment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Braintree\Model\Ui\ConfigProvider; + +/** + * @var Magento\Quote\Model\Quote $quote + */ + +if (empty($quote)) { + throw new \Exception('$quote should be defined in the parent fixture'); +} + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var PaymentInterface $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod(ConfigProvider::CODE); +$quote->setPayment($payment); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree_paypal.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree_paypal.php new file mode 100644 index 0000000000000..e4bba222078b0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree_paypal.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote\Payment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; + +/** + * @var Magento\Quote\Model\Quote $quote + */ + +if (empty($quote)) { + throw new \Exception('$quote should be defined in the parent fixture'); +} + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var PaymentInterface $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod(ConfigProvider::PAYPAL_CODE); +$quote->setPayment($payment); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree.php new file mode 100644 index 0000000000000..1c56e611dd6db --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); + +/** @var Quote $quote */ +$quote = $objectManager->create(Quote::class); + +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/shipping_address_list.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/billing_address.php'; +require __DIR__ . '/payment_braintree.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/items.php'; +require __DIR__ . '/assign_items_per_address.php'; diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php new file mode 100644 index 0000000000000..4bd8e926abb76 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); + +/** @var Quote $quote */ +$quote = $objectManager->create(Quote::class); + +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/shipping_address_list.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/billing_address.php'; +require __DIR__ . '/payment_braintree_paypal.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/items.php'; +require __DIR__ . '/assign_items_per_address.php'; diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Model/MultishippingTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Model/MultishippingTest.php new file mode 100644 index 0000000000000..91bc0388d8551 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Model/MultishippingTest.php @@ -0,0 +1,254 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Model; + +use Braintree\Result\Successful; +use Braintree\Transaction; +use Magento\Braintree\Gateway\Command\GetPaymentNonceCommand; +use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Multishipping\Model\Checkout\Type\Multishipping; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use \PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Payment\Gateway\Command\ResultInterface as CommandResultInterface; + +/** + * Tests Magento\Multishipping\Model\Checkout\Type\Multishipping with Braintree and BraintreePayPal payments. + * + * @magentoAppArea frontend + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class MultishippingTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var BraintreeAdapter|MockObject + */ + private $adapter; + + /** + * @var Multishipping + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + + $orderSender = $this->getMockBuilder(OrderSender::class) + ->disableOriginalConstructor() + ->getMock(); + + $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->adapter = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactory->method('create') + ->willReturn($this->adapter); + + $this->objectManager->addSharedInstance($adapterFactory, BraintreeAdapterFactory::class); + $this->objectManager->addSharedInstance($this->getPaymentNonceMock(), GetPaymentNonceCommand::class); + + $this->model = $this->objectManager->create( + Multishipping::class, + ['orderSender' => $orderSender] + ); + } + + /** + * Checks a case when multiple orders are created successfully using Braintree payment method. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Braintree/Fixtures/quote_with_split_items_braintree.php + * @magentoConfigFixture current_store payment/braintree/active 1 + * @return void + */ + public function testCreateOrdersWithBraintree() + { + $this->adapter->method('sale') + ->willReturn( + $this->getTransactionStub() + ); + $this->createOrders(); + } + + /** + * Checks a case when multiple orders are created successfully using Braintree PayPal payment method. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php + * @magentoConfigFixture current_store payment/braintree_paypal/active 1 + * @return void + */ + public function testCreateOrdersWithBraintreePaypal() + { + $this->adapter->method('sale') + ->willReturn( + $this->getTransactionPaypalStub() + ); + $this->createOrders(); + } + + /** + * Creates orders for multishipping checkout flow. + * + * @return void + */ + private function createOrders() + { + $expectedPlacedOrdersNumber = 3; + $quote = $this->getQuote('multishipping_quote_id_braintree'); + + /** @var CheckoutSession $session */ + $session = $this->objectManager->get(CheckoutSession::class); + $session->replaceQuote($quote); + + $this->model->createOrders(); + + $orderList = $this->getOrderList((int)$quote->getId()); + self::assertCount( + $expectedPlacedOrdersNumber, + $orderList, + 'Total successfully placed orders number mismatch' + ); + } + + /** + * Creates stub for Braintree capture Transaction. + * + * @return Successful + */ + private function getTransactionStub(): Successful + { + $transaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->getMock(); + $transaction->status = 'submitted_for_settlement'; + $transaction->creditCard = [ + 'last4' => '1111', + 'cardType' => 'Visa', + 'expirationMonth' => '12', + 'expirationYear' => '2021' + ]; + + $creditCardDetails = new \stdClass(); + $creditCardDetails->token = '4fdg'; + $creditCardDetails->expirationMonth = '12'; + $creditCardDetails->expirationYear = '2021'; + $creditCardDetails->cardType = 'Visa'; + $creditCardDetails->last4 = '1111'; + $creditCardDetails->expirationDate = '12/2021'; + $transaction->creditCardDetails = $creditCardDetails; + + $response = new Successful(); + $response->success = true; + $response->transaction = $transaction; + + return $response; + } + + /** + * Creates stub for BraintreePaypal capture Transaction. + * + * @return Successful + */ + private function getTransactionPaypalStub(): Successful + { + $transaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->getMock(); + $transaction->status = 'submitted_for_settlement'; + $transaction->paypal = [ + 'token' => 'fchxqx', + 'payerEmail' => 'payer@example.com', + 'paymentId' => 'PAY-33ac47a28e7f54791f6cda45', + ]; + $paypalDetails = new \stdClass(); + $paypalDetails->token = 'fchxqx'; + $paypalDetails->payerEmail = 'payer@example.com'; + $paypalDetails->paymentId = '33ac47a28e7f54791f6cda45'; + $transaction->paypalDetails = $paypalDetails; + + $response = new Successful(); + $response->success = true; + $response->transaction = $transaction; + + return $response; + } + + /** + * Retrieves quote by reserved order id. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuote(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + return array_pop($items); + } + + /** + * Get list of orders by quote id. + * + * @param int $quoteId + * @return array + */ + private function getOrderList(int $quoteId): array + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('quote_id', $quoteId) + ->create(); + + /** @var OrderRepositoryInterface $orderRepository */ + $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + return $orderRepository->getList($searchCriteria)->getItems(); + } + + /** + * Returns GetPaymentNonceCommand command mock. + * + * @return MockObject + */ + private function getPaymentNonceMock(): MockObject + { + $commandResult = $this->createMock(CommandResultInterface::class); + $commandResult->method('get') + ->willReturn(['paymentMethodNonce' => 'testNonce']); + $paymentNonce = $this->createMock(GetPaymentNonceCommand::class); + $paymentNonce->method('execute') + ->willReturn($commandResult); + + return $paymentNonce; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundlePriceCalculatorWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundlePriceCalculatorWithDimensionTest.php index 6794a686146f9..c1750332fa568 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundlePriceCalculatorWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/DynamicBundlePriceCalculatorWithDimensionTest.php @@ -8,7 +8,7 @@ /** * @magentoDbIsolation disabled - * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group + * @--magentoIndexerDimensionMode catalog_product_price website_and_customer_group * @group indexer_dimension * @magentoAppArea frontend */ @@ -24,6 +24,9 @@ class DynamicBundlePriceCalculatorWithDimensionTest extends BundlePriceAbstract */ public function testPriceForDynamicBundle(array $strategyModifiers, array $expectedResults) { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->prepareFixture($strategyModifiers, 'bundle_product'); $bundleProduct = $this->productRepository->get('bundle_product', false, null, true); @@ -63,6 +66,9 @@ public function testPriceForDynamicBundle(array $strategyModifiers, array $expec */ public function testPriceForDynamicBundleInWebsiteScope(array $strategyModifiers, array $expectedResults) { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->prepareFixture($strategyModifiers, 'bundle_product'); $bundleProduct = $this->productRepository->get('bundle_product', false, null, true); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php index ffc24b2f45d5c..9e7c8d9836c74 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php @@ -10,7 +10,7 @@ /** * @magentoDbIsolation disabled - * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group + * @--magentoIndexerDimensionMode catalog_product_price website_and_customer_group * @group indexer_dimension * @magentoAppArea frontend */ @@ -26,6 +26,9 @@ class FixedBundlePriceCalculatorWithDimensionTest extends BundlePriceAbstract */ public function testPriceForFixedBundle(array $strategyModifiers, array $expectedResults) { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->prepareFixture($strategyModifiers, 'bundle_product'); $bundleProduct = $this->productRepository->get('bundle_product', false, null, true); @@ -65,6 +68,9 @@ public function testPriceForFixedBundle(array $strategyModifiers, array $expecte */ public function testPriceForFixedBundleInWebsiteScope(array $strategyModifiers, array $expectedResults) { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->prepareFixture($strategyModifiers, 'bundle_product'); $bundleProduct = $this->productRepository->get('bundle_product', false, null, true); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php index 2a68ff48e5f9a..4a5757aae3134 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php @@ -6,7 +6,7 @@ namespace Magento\Bundle\Model\Product; /** - * @magentoDataFixture Magento/Bundle/_files/product_with_tier_pricing.php + * Class to test bundle prices */ class PriceTest extends \PHPUnit\Framework\TestCase { @@ -22,6 +22,9 @@ protected function setUp() ); } + /** + * @magentoDataFixture Magento/Bundle/_files/product_with_tier_pricing.php + */ public function testGetTierPrice() { /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ @@ -37,4 +40,50 @@ public function testGetTierPrice() $this->assertEquals(20.0, $this->_model->getTierPrice(4, $product)); $this->assertEquals(30.0, $this->_model->getTierPrice(5, $product)); } + + /** + * Test calculation final price for bundle product with tire price in simple product + * + * @param float $bundleQty + * @param float $selectionQty + * @param float $finalPrice + * @magentoDataFixture Magento/Bundle/_files/product_with_simple_tier_pricing.php + * @dataProvider getSelectionFinalTotalPriceWithSimpleTierPriceDataProvider + */ + public function testGetSelectionFinalTotalPriceWithSimpleTierPrice( + float $bundleQty, + float $selectionQty, + float $finalPrice + ) { + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $bundleProduct = $productRepository->get('bundle-product'); + $simpleProduct = $productRepository->get('simple'); + $simpleProduct->setCustomerGroupId(\Magento\Customer\Model\Group::CUST_GROUP_ALL); + + $this->assertEquals( + $finalPrice, + $this->_model->getSelectionFinalTotalPrice( + $bundleProduct, + $simpleProduct, + $bundleQty, + $selectionQty, + false + ), + 'Tier price calculation for Simple product is wrong' + ); + } + + /** + * @return array + */ + public function getSelectionFinalTotalPriceWithSimpleTierPriceDataProvider(): array + { + return [ + [1, 1, 10], + [2, 1, 8], + [5, 1, 5], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceWithDimensionTest.php index 3b3b1ed5cbd07..a1516c6a74808 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceWithDimensionTest.php @@ -7,7 +7,7 @@ /** * @magentoDbIsolation disabled - * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group + * @--magentoIndexerDimensionMode catalog_product_price website_and_customer_group * @group indexer_dimension * @magentoDataFixture Magento/Bundle/_files/product_with_tier_pricing.php */ @@ -27,6 +27,9 @@ protected function setUp() public function testGetTierPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing.php new file mode 100644 index 0000000000000..30f0978480701 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; + +/** @var $productFactory Magento\Catalog\Model\ProductFactory */ +$productFactory = $objectManager->create(\Magento\Catalog\Model\ProductFactory::class); +/** @var $bundleProduct \Magento\Catalog\Model\Product */ +$bundleProduct = $productFactory->create(); +$bundleProduct->setTypeId('bundle') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setPriceType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC) + ->setPriceView(1) + ->setName('Bundle Product') + ->setSku('bundle-product') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData([ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ]) + ->setBundleOptionsData( + [ + [ + 'title' => 'Bundle Product Items', + 'default_title' => 'Bundle Product Items', + 'type' => 'checkbox', + 'required' => 1, + 'delete' => '', + ], + ] + ) + ->setBundleSelectionsData( + [[['product_id' => $product->getId(), 'selection_qty' => 1, 'delete' => '']]] + ); +$productRepository->save($bundleProduct); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing_rollback.php new file mode 100644 index 0000000000000..aa661c7412d42 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing_rollback.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_rollback.php'; + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $product = $productRepository->get('bundle-product'); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Captcha/Observer/CaseCaptchaIsRequiredAfterFailedLoginAttemptsTest.php b/dev/tests/integration/testsuite/Magento/Captcha/Observer/CaseCaptchaIsRequiredAfterFailedLoginAttemptsTest.php index 32fab1f456b3e..4c469421402c2 100644 --- a/dev/tests/integration/testsuite/Magento/Captcha/Observer/CaseCaptchaIsRequiredAfterFailedLoginAttemptsTest.php +++ b/dev/tests/integration/testsuite/Magento/Captcha/Observer/CaseCaptchaIsRequiredAfterFailedLoginAttemptsTest.php @@ -7,9 +7,14 @@ use Magento\Framework\Message\MessageInterface; +/** + * @magentoAppArea adminhtml + */ class CaseCaptchaIsRequiredAfterFailedLoginAttemptsTest extends \Magento\TestFramework\TestCase\AbstractController { /** + * Tests backend login action with invalid captcha. + * * @magentoAdminConfigFixture admin/captcha/forms backend_login * @magentoAdminConfigFixture admin/captcha/enable 1 * @magentoAdminConfigFixture admin/captcha/mode always diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php index c97dc821913f9..eb08509a9e36d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php @@ -12,6 +12,7 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Framework\View\LayoutInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Block\Product\View\GalleryOptions; class GalleryTest extends \PHPUnit\Framework\TestCase { @@ -41,10 +42,12 @@ public function testHtml() /** @var Gallery $block */ $block = $layout->createBlock(Gallery::class); $block->setData('product', $product); + $galleryoptions = $this->objectManager->get(GalleryOptions::class); + $block->setData('gallery_options', $galleryoptions); $block->setTemplate("Magento_Catalog::product/view/gallery.phtml"); $showCaption = $block->getVar('gallery/caption'); - self::assertContains('"showCaption": ' . $showCaption, $block->toHtml()); + self::assertContains('"showCaption":' . $showCaption, $block->toHtml()); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index a6cffda80e705..bdf2486ac2e38 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -8,6 +8,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Store\Model\Store; use Magento\Catalog\Model\ResourceModel\Product; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml @@ -51,6 +52,7 @@ public function testSaveAction($inputData, $defaultAttributes, $attributesSaved $store->load('fixturestore', 'code'); $storeId = $store->getId(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($inputData); $this->getRequest()->setParam('store', $storeId); $this->getRequest()->setParam('id', 2); @@ -99,6 +101,7 @@ public function testSaveAction($inputData, $defaultAttributes, $attributesSaved */ public function testSaveActionFromProductCreationPage($postData) { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/category/save'); @@ -357,6 +360,7 @@ public function saveActionDataProvider() */ public function testSaveActionCategoryWithDangerRequest() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'general' => [ @@ -407,7 +411,8 @@ public function testMoveAction($parentId, $childId, $childUrlKey, $grandChildId, } $this->getRequest() ->setPostValue('id', $grandChildId) - ->setPostValue('pid', $parentId); + ->setPostValue('pid', $parentId) + ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/category/move'); $jsonResponse = json_decode($this->getResponse()->getBody()); $this->assertNotNull($jsonResponse); @@ -444,6 +449,7 @@ public function testSaveCategoryWithProductPosition(array $postData) $this->getRequest()->setParam('store', $storeId); $this->getRequest()->setParam('id', 96377); $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/category/save'); $newCategoryProductsCount = $this->getCategoryProductsCount(); $this->assertEquals( @@ -543,10 +549,8 @@ private function getCategoryProductsCount(): int { $oldCategoryProducts = $this->productResource->getConnection()->select()->from( $this->productResource->getTable('catalog_category_product'), - 'product_id' - ); - return count( - $this->productResource->getConnection()->fetchAll($oldCategoryProducts) + new \Zend_Db_Expr('COUNT(product_id)') ); + return $this->productResource->getConnection()->fetchOne($oldCategoryProducts); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php index cea49d940cb62..3d7575729cd92 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * @magentoAppArea adminhtml */ @@ -23,6 +25,7 @@ public function testSaveActionRedirectsSuccessfully() /** @var $session \Magento\Backend\Model\Session */ $session = $objectManager->get(\Magento\Backend\Model\Session::class); $session->setProductIds([1]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); @@ -69,6 +72,7 @@ public function testSaveActionChangeVisibility($attributes) $session = $objectManager->get(\Magento\Backend\Model\Session::class); $session->setProductIds([$product->getId()]); $this->getRequest()->setParam('attributes', $attributes); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); /** @var \Magento\Catalog\Model\Category $category */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php index 45c1583d76400..169b6fa52e307 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php @@ -6,10 +6,13 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml * @magentoDbIsolation enabled + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -18,10 +21,14 @@ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendContr */ public function testWrongFrontendInput() { - $postData = $this->_getAttributeData() + [ + $postData = array_merge( + $this->_getAttributeData(), + [ 'attribute_id' => 100500, 'frontend_input' => 'some_input', - ]; + ] + ); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); @@ -47,6 +54,7 @@ public function testWithPopup() 'popup' => 'true', 'new_attribute_set_name' => 'new_attribute_set', ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); @@ -68,6 +76,7 @@ public function testWithExceptionWhenSaveAttribute() { $postData = $this->_getAttributeData() + ['attribute_id' => 0, 'frontend_input' => 'boolean']; $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); $this->assertContains( @@ -86,6 +95,7 @@ public function testWrongAttributeId() { $postData = $this->_getAttributeData() + ['attribute_id' => 100500]; $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); $this->assertContains( @@ -110,6 +120,7 @@ public function testAttributeWithoutId() 'set' => 4, 'frontend_input' => 'boolean', ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); @@ -132,6 +143,7 @@ public function testWrongAttributeCode() { $postData = $this->_getAttributeData() + ['attribute_code' => '_()&&&?']; $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); $this->assertContains( @@ -156,6 +168,7 @@ public function testWrongAttributeCode() public function testAttributeWithoutEntityTypeId() { $postData = $this->_getAttributeData() + ['attribute_id' => '2', 'new_attribute_set_name' => ' ']; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); @@ -171,6 +184,7 @@ public function testAttributeWithoutEntityTypeId() public function testSaveActionApplyToDataSystemAttribute() { $postData = $this->_getAttributeData() + ['attribute_id' => '2']; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); @@ -184,6 +198,7 @@ public function testSaveActionApplyToDataSystemAttribute() public function testSaveActionApplyToDataUserDefinedAttribute() { $postData = $this->_getAttributeData() + ['attribute_id' => '1']; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $model */ @@ -199,6 +214,7 @@ public function testSaveActionApplyToData() { $postData = $this->_getAttributeData() + ['attribute_id' => '3']; unset($postData['apply_to']); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); @@ -218,6 +234,7 @@ public function testSaveActionCleanAttributeLabelCache() $this->assertEquals('predefined string translation', $this->_translate('string to translate')); $string->saveTranslate('string to translate', 'new string translation'); $postData = $this->_getAttributeData() + ['attribute_id' => 1]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals('new string translation', $this->_translate('string to translate')); @@ -293,6 +310,7 @@ public function testLargeOptionsDataSet() $optionsData[] = http_build_query($optionRowData); } $attributeData['serialized_options'] = json_encode($optionsData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($attributeData); $this->dispatch('backend/catalog/product_attribute/save'); $entityTypeId = $this->_objectManager->create( @@ -364,6 +382,7 @@ protected function _getAttributeData() 'default_value_textarea' => '0', 'is_required' => '1', 'frontend_class' => '', + 'frontend_input' => 'select', 'is_searchable' => '0', 'is_visible_in_advanced_search' => '0', 'is_comparable' => '0', diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php index 7c5d4ea48a238..7e034b8b3cb7e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; use Magento\Framework\Message\MessageInterface; +use Magento\Framework\App\Request\Http as HttpRequest; class DeleteTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -15,7 +16,7 @@ class DeleteTest extends \Magento\TestFramework\TestCase\AbstractBackendControll public function testDeleteById() { $attributeSet = $this->getAttributeSetByName('empty_attribute_set'); - $this->getRequest()->setParam('id', $attributeSet->getId()); + $this->getRequest()->setParam('id', $attributeSet->getId())->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_set/delete/'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php index 5b711b2ea7418..8ccd426424a29 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php @@ -9,6 +9,7 @@ use Magento\Eav\Api\Data\AttributeSetInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; class SaveTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -20,6 +21,7 @@ public function testAlreadyExistsExceptionProcessingWhenGroupCodeIsDuplicated() $attributeSet = $this->getAttributeSetByName('attribute_set_test'); $this->assertNotEmpty($attributeSet, 'Attribute set with name "attribute_set_test" is missed'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue('data', json_encode([ 'attribute_set_name' => 'attribute_set_test', 'groups' => [ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php index 4761f13175d81..44577b2a228a0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php @@ -11,6 +11,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Message\MessageInterface; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml @@ -19,6 +20,7 @@ class ProductTest extends \Magento\TestFramework\TestCase\AbstractBackendControl { public function testSaveActionWithDangerRequest() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue(['product' => ['entity_id' => 15]]); $this->dispatch('backend/catalog/product/save'); $this->assertSessionMessages( @@ -36,6 +38,7 @@ public function testSaveActionAndNew() $this->getRequest()->setPostValue(['back' => 'new']); $repository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); $product = $repository->get('simple'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); $this->assertRedirect($this->stringStartsWith('http://localhost/index.php/backend/catalog/product/new/')); $this->assertSessionMessages( @@ -52,6 +55,7 @@ public function testSaveActionAndDuplicate() $this->getRequest()->setPostValue(['back' => 'duplicate']); $repository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); $product = $repository->get('simple'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); $this->assertRedirect($this->stringStartsWith('http://localhost/index.php/backend/catalog/product/edit/')); $this->assertRedirect( @@ -161,11 +165,13 @@ public function testEditAction() public function testSaveActionWithAlreadyExistingUrlKey(array $postData) { $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product/save'); /** @var Manager $messageManager */ $messageManager = $this->_objectManager->get(Manager::class); $messages = $messageManager->getMessages(); $errors = $messages->getItemsByType('error'); + $this->assertNotEmpty($errors); $message = array_shift($errors); $this->assertSame('URL key for specified store already exists.', $message->getText()); $this->assertRedirect($this->stringContains('/backend/catalog/product/new')); @@ -233,7 +239,6 @@ public function saveActionWithAlreadyExistingUrlKeyDataProvider() 'thumbnail' => '/m/a//magento_image.jpg.tmp', 'swatch_image' => '/m/a//magento_image.jpg.tmp', ], - 'form_key' => Bootstrap::getObjectManager()->get(FormKey::class)->getFormKey(), ] ] ]; @@ -251,6 +256,7 @@ public function saveActionWithAlreadyExistingUrlKeyDataProvider() public function testSaveActionTierPrice(array $postData, array $tierPrice) { $postData['product'] = $this->getProductData($tierPrice); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product/save/id/' . $postData['id']); $this->assertSessionMessages( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php index 7e27fd7ede8b4..f9b1d10cbb8ae 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php @@ -7,7 +7,9 @@ // @codingStandardsIgnoreFile namespace Magento\Catalog\Controller\Product; + use Magento\Framework\Message\MessageInterface; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoDataFixture Magento/Catalog/controllers/_files/products.php @@ -23,6 +25,9 @@ class CompareTest extends \Magento\TestFramework\TestCase\AbstractController */ protected $productRepository; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); @@ -40,6 +45,7 @@ public function testAddAction() /** @var \Magento\Framework\Data\Form\FormKey $formKey */ $formKey = $objectManager->get(\Magento\Framework\Data\Form\FormKey::class); $product = $this->productRepository->get('simple_product_1'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch( sprintf( 'catalog/product_compare/add/product/%s/form_key/%s?nocookie=1', @@ -49,7 +55,12 @@ public function testAddAction() ); $this->assertSessionMessages( - $this->equalTo(['You added product Simple Product 1 Name to the <a href="http://localhost/index.php/catalog/product_compare/">comparison list</a>.']), + $this->equalTo( + [ + 'You added product Simple Product 1 Name to the '. + '<a href="http://localhost/index.php/catalog/product_compare/">comparison list</a>.' + ] + ), MessageInterface::TYPE_SUCCESS ); @@ -73,6 +84,7 @@ public function testRemoveAction() { $this->_requireVisitorWithTwoProducts(); $product = $this->productRepository->get('simple_product_2'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('catalog/product_compare/remove/product/' . $product->getEntityId()); $this->assertSessionMessages( @@ -89,6 +101,7 @@ public function testRemoveActionWithSession() { $this->_requireCustomerWithTwoProducts(); $product = $this->productRepository->get('simple_product_1'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('catalog/product_compare/remove/product/' . $product->getEntityId()); $secondProduct = $this->productRepository->get('simple_product_2'); @@ -132,6 +145,7 @@ public function testClearAction() { $this->_requireVisitorWithTwoProducts(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('catalog/product_compare/clear'); $this->assertSessionMessages( @@ -151,6 +165,7 @@ public function testRemoveActionProductNameXss() { $this->_prepareCompareListWithProductNameXss(); $product = $this->productRepository->get('product-with-xss'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('catalog/product_compare/remove/product/' . $product->getEntityId() . '?nocookie=1'); $this->assertSessionMessages( @@ -307,7 +322,8 @@ protected function _assertCompareListEquals(array $expectedProductIds) // important $compareItems->setVisitorId( \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Customer\Model\Visitor::class)->getId() + \Magento\Customer\Model\Visitor::class + )->getId() ); $actualProductIds = []; foreach ($compareItems as $compareItem) { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Price/SimpleWithOptionsTierPriceWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Price/SimpleWithOptionsTierPriceWithDimensionTest.php index 40607cd85b3b4..e70fa8f52d269 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Price/SimpleWithOptionsTierPriceWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Price/SimpleWithOptionsTierPriceWithDimensionTest.php @@ -42,11 +42,14 @@ protected function setUp() /** * @magentoDbIsolation disabled - * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group + * @--magentoIndexerDimensionMode catalog_product_price website_and_customer_group * @magentoDataFixture Magento/Catalog/_files/category_product.php */ public function testTierPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $tierPriceValue = 9.00; $tierPrice = $this->objectManager->create(ProductTierPriceInterfaceFactory::class) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php index 12b7da2bd6e35..e0f26a7aab01b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php @@ -17,7 +17,7 @@ /** * @magentoDbIsolation disabled - * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group + * @--magentoIndexerDimensionMode catalog_product_price website_and_customer_group * @group indexer_dimension * @magentoDataFixture Magento/Catalog/_files/product_simple.php */ @@ -37,6 +37,9 @@ protected function setUp() public function testGetPriceFromIndexer() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); /** @var PriceTableResolver $tableResolver */ $tableResolver = Bootstrap::getObjectManager()->create(PriceTableResolver::class); @@ -66,11 +69,17 @@ public function testGetPriceFromIndexer() public function testGetPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->assertEquals('test', $this->_model->getPrice(new DataObject(['price' => 'test']))); } public function testGetFinalPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $repository = Bootstrap::getObjectManager()->create( ProductRepository::class ); @@ -95,6 +104,9 @@ public function testGetFinalPrice() public function testGetFormatedPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $repository = Bootstrap::getObjectManager()->create( ProductRepository::class ); @@ -105,12 +117,18 @@ public function testGetFormatedPrice() public function testCalculatePrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->assertEquals(10, $this->_model->calculatePrice(10, 8, '1970-12-12 23:59:59', '1971-01-01 01:01:01')); $this->assertEquals(8, $this->_model->calculatePrice(10, 8, '1970-12-12 23:59:59', '2034-01-01 01:01:01')); } public function testCalculateSpecialPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->assertEquals( 10, $this->_model->calculateSpecialPrice(10, 8, '1970-12-12 23:59:59', '1971-01-01 01:01:01') @@ -123,6 +141,9 @@ public function testCalculateSpecialPrice() public function testIsTierPriceFixed() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->assertTrue($this->_model->isTierPriceFixed()); } @@ -134,6 +155,9 @@ public function testIsTierPriceFixed() */ private function prepareBuyRequest(Product $product) { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $options = []; /** @var $option \Magento\Catalog\Model\Product\Option */ foreach ($product->getOptions() as $option) { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceWithDimensionTest.php index 9e3db8c155e28..cb776fb08723f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductPriceWithDimensionTest.php @@ -15,7 +15,7 @@ * - pricing behaviour is tested * @group indexer_dimension * @magentoDbIsolation disabled - * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group + * @--magentoIndexerDimensionMode catalog_product_price website_and_customer_group * @see \Magento\Catalog\Model\ProductTest * @see \Magento\Catalog\Model\ProductExternalTest */ @@ -39,6 +39,9 @@ protected function setUp() public function testGetPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->assertEmpty($this->_model->getPrice()); $this->_model->setPrice(10.0); $this->assertEquals(10.0, $this->_model->getPrice()); @@ -46,6 +49,9 @@ public function testGetPrice() public function testGetPriceModel() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $default = $this->_model->getPriceModel(); $this->assertInstanceOf(\Magento\Catalog\Model\Product\Type\Price::class, $default); $this->assertSame($default, $this->_model->getPriceModel()); @@ -56,6 +62,9 @@ public function testGetPriceModel() */ public function testGetTierPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->assertEquals([], $this->_model->getTierPrice()); } @@ -64,6 +73,9 @@ public function testGetTierPrice() */ public function testGetTierPriceCount() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->assertEquals(0, $this->_model->getTierPriceCount()); } @@ -72,11 +84,17 @@ public function testGetTierPriceCount() */ public function testGetFormatedPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->assertEquals('<span class="price">$0.00</span>', $this->_model->getFormatedPrice()); } public function testSetGetFinalPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->assertEquals(0, $this->_model->getFinalPrice()); $this->_model->setPrice(10); $this->_model->setFinalPrice(10); @@ -88,6 +106,9 @@ public function testSetGetFinalPrice() */ public function testGetMinPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $product = $this->productRepository->get('simple'); $collection = Bootstrap::getObjectManager()->create(Collection::class); $collection->addIdFilter($product->getId()); @@ -103,6 +124,9 @@ public function testGetMinPrice() */ public function testGetMinPriceForComposite() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $confProduct = $this->productRepository->get('configurable'); $collection = Bootstrap::getObjectManager()->create(Collection::class); $collection->addIdFilter($confProduct->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php index 7954e2c36227f..476f01eb277df 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php @@ -12,6 +12,11 @@ class ProductTest extends TestCase { + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @var Product */ @@ -29,7 +34,8 @@ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $this->model = $this->objectManager->get(Product::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->model = $this->objectManager->create(Product::class); } /** @@ -42,11 +48,29 @@ public function testGetAttributeRawValue() $sku = 'simple'; $attribute = 'name'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($sku); - + $product = $this->productRepository->get($sku); $actual = $this->model->getAttributeRawValue($product->getId(), $attribute, null); self::assertEquals($product->getName(), $actual); } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store catalog/price/scope 1 + */ + public function testUpdateStoreSpecificSpecialPrice() + { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get('simple', true, 1); + $this->assertEquals(5.99, $product->getSpecialPrice()); + + $product->setSpecialPrice(''); + $this->model->save($product); + $product = $this->productRepository->get('simple', false, 1, true); + $this->assertEmpty($product->getSpecialPrice()); + + $product = $this->productRepository->get('simple', false, 0, true); + $this->assertEquals(5.99, $product->getSpecialPrice()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php new file mode 100644 index 0000000000000..23fd8d7fe324e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$stockDataConfig = [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1 +]; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); + +/** @var ProductInterface $product */ +$product = $objectManager->create(ProductInterface::class); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Чудовий продукт без Url Key') + ->setSku('ukrainian-without-url-key') + ->setPrice(10) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([2]) + ->setStockData($stockDataConfig); +try { + $productRepository->save($product); +} catch (\Exception $e) { + // problems during save +}; + +/** @var ProductInterface $product */ +$product = $objectManager->create(ProductInterface::class); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Надзвичайний продукт з Url Key') + ->setSku('ukrainian-with-url-key') + ->setPrice(10) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([2]) + ->setStockData($stockDataConfig) + ->setUrlKey('надзвичайний продукт на кожен день'); +try { + $productRepository->save($product); +} catch (\Exception $e) { + // problems during save +}; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key_rollback.php new file mode 100644 index 0000000000000..d4592430c0e94 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_non_latin_url_key_rollback.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); + +$productSkus = [ + 'ukrainian-with-url-key', + 'ukrainian-without-url-key', +]; +try { + foreach ($productSkus as $sku) { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } +} catch (NoSuchEntityException $e) { + // nothing to delete +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php index 15e274541bac4..a5ca4573d98b0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php @@ -15,8 +15,8 @@ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setWebsiteIds([1]) ->setStockData(['qty' => 100, 'is_in_stock' => 1]) - ->setNewsFromDate(date('Y-m-d', strtotime('-2 day'))) - ->setNewsToDate(date('Y-m-d', strtotime('+2 day'))) + ->setNewsFromDate(date('Y-m-d H:i:s', strtotime('-2 day'))) + ->setNewsToDate(date('Y-m-d H:i:s', strtotime('+2 day'))) ->setDescription('description') ->setShortDescription('short desc') ->save(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 6e361dfb05de0..cb96910ec86e1 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -28,11 +28,13 @@ use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\Store\Model\Store; use Psr\Log\LoggerInterface; +use Magento\Framework\Exception\NoSuchEntityException; /** * Class ProductTest * @magentoAppIsolation enabled * @magentoDbIsolation enabled + * @magentoAppArea adminhtml * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_reindex_schedule.php * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -71,6 +73,11 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase */ private $logger; + /** + * @var array + */ + private $importedProducts; + /** * @inheritdoc */ @@ -84,10 +91,27 @@ protected function setUp() \Magento\CatalogImportExport\Model\Import\Product::class, ['logger' => $this->logger] ); + $this->importedProducts = []; parent::setUp(); } + protected function tearDown() + { + /* We rollback here the products created during the Import because they were + created during test execution and we do not have the rollback for them */ + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + foreach ($this->importedProducts as $productSku) { + try { + $product = $productRepository->get($productSku, false, null, true); + $productRepository->delete($product); + } catch (NoSuchEntityException $e) { + // nothing to delete + } + } + } + /** * Options for assertion * @@ -271,6 +295,8 @@ public function testStockState() * @param string $importFile * @param string $sku * @param int $expectedOptionsQty + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException * @magentoAppIsolation enabled */ public function testSaveCustomOptions($importFile, $sku, $expectedOptionsQty) @@ -1244,6 +1270,8 @@ public function testProductPositionInCategory() * @magentoAppIsolation enabled * @magentoDataFixture Magento/Catalog/_files/category_product.php * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testNewProductPositionInCategory() { @@ -1369,6 +1397,7 @@ protected function loadCategoryByName($categoryName) * @dataProvider validateUrlKeysDataProvider * @param $importFile string * @param $expectedErrors array + * @throws \Magento\Framework\Exception\LocalizedException */ public function testValidateUrlKeys($importFile, $expectedErrors) { @@ -1597,12 +1626,13 @@ public function testImportWithoutUrlKeys() * @magentoDbIsolation disabled * @magentoAppIsolation enabled * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ public function testImportWithUrlKeysWithSpaces() { $products = [ - 'simple1' => 'url-key-with-spaces1', - 'simple2' => 'url-key-with-spaces2', + 'simple1' => 'url key with spaces1', + 'simple2' => 'url key with spaces2', ]; $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); @@ -1629,6 +1659,52 @@ public function testImportWithUrlKeysWithSpaces() } } + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_non_latin_url_key.php + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testImportWithNonLatinUrlKeys() + { + $productsCreatedByFixture = [ + 'ukrainian-with-url-key' => 'nove-im-ja-pislja-importu-scho-stane-url-key', + 'ukrainian-without-url-key' => 'новий url key після імпорту', + ]; + $productsImportedByCsv = [ + 'imported-ukrainian-with-url-key' => 'імпортований продукт', + 'imported-ukrainian-without-url-key' => 'importovanij-produkt-bez-url-key', + ]; + $productSkuMap = array_merge($productsCreatedByFixture, $productsImportedByCsv); + $this->importedProducts = array_keys($productsImportedByCsv); + + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/products_to_import_with_non_latin_url_keys.csv', + 'directory' => $directory, + ] + ); + + $errors = $this->_model->setParameters( + ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE, 'entity' => 'catalog_product'] + ) + ->setSource($source) + ->validateData(); + + $this->assertEquals($errors->getErrorsCount(), 0); + $this->_model->importData(); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + foreach ($productSkuMap as $productSku => $productUrlKey) { + $this->assertEquals($productUrlKey, $productRepository->get($productSku)->getUrlKey()); + } + } + /** * Make sure the absence of a url_key column in the csv file won't erase the url key of the existing products. * To reach the goal we need to not send the name column, as the url key is generated from it. @@ -1844,6 +1920,7 @@ public function testProductWithWrappedAdditionalAttributes() * * @param string $fileName * @param int $expectedErrors + * @throws \Magento\Framework\Exception\LocalizedException */ private function importDataForMediaTest(string $fileName, int $expectedErrors = 0) { @@ -2266,6 +2343,7 @@ public function testImportWithBackordersDisabled() * Import file by providing import filename in parameters * * @param string $fileName + * @throws \Magento\Framework\Exception\LocalizedException */ private function importFile(string $fileName) { @@ -2297,6 +2375,7 @@ private function importFile(string $fileName) * Import file with non-existing images and skip-errors strategy. * * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ public function testImportWithSkipErrorsAndNonExistingImage() { @@ -2382,6 +2461,7 @@ public function testImportProductWithUpdateUrlKey() * @magentoDataFixture mediaImportImageFixture * @magentoAppIsolation enabled * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ public function testSaveProductOnImportNonExistingImage() { @@ -2414,6 +2494,8 @@ public function testSaveProductOnImportNonExistingImage() * @magentoDbIsolation disabled * @magentoAppIsolation enabled * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testImportProductWithContinueOnError() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_non_latin_url_keys.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_non_latin_url_keys.csv new file mode 100644 index 0000000000000..8b324a5330779 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_non_latin_url_keys.csv @@ -0,0 +1,5 @@ +sku,product_type,store_view_code,name,price,attribute_set_code,url_key +imported-ukrainian-with-url-key,simple,,"Імпортований продукт з Url Key",50,Default,"імпортований продукт" +imported-ukrainian-without-url-key,simple,,"Імпортований продукт без Url Key",55,Default, +ukrainian-without-url-key,simple,,"Чудовий продукт без Url Key",55,Default,"новий url key після імпорту" +ukrainian-with-url-key,simple,,"Нове ім'я після імпорту що стане url key",55,Default, \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php b/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php index 08e9ebbd1f9f0..5774f1cf76ae9 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php @@ -73,21 +73,6 @@ public function testCreateCollection() $this->performAssertions(2); } - /** - * Test product list widget can process condition with multiple product sku. - * - * @magentoDbIsolation disabled - * @magentoDataFixture Magento/Catalog/_files/multiple_products.php - */ - public function testCreateCollectionWithMultipleSkuCondition() - { - $encodedConditions = '^[`1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Combine`,' . - '`aggregator`:`all`,`value`:`1`,`new_child`:``^],`1--1`:^[`type`:`Magento||CatalogWidget||Model||Rule|' . - '|Condition||Product`,`attribute`:`sku`,`operator`:`==`,`value`:`simple1, simple2`^]^]'; - $this->block->setData('conditions_encoded', $encodedConditions); - $this->performAssertions(2); - } - /** * Test product list widget can process condition with dropdown type of attribute * diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php new file mode 100644 index 0000000000000..26d9651263cef --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Api; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +/** + * Test GuestShippingInformationManagement API. + */ +class GuestShippingInformationManagementTest extends TestCase +{ + /** + * @var GuestShippingInformationManagementInterface + */ + private $management; + + /** + * @var CartRepositoryInterface + */ + private $cartRepo; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepo; + + /** + * @var ShippingInformationInterfaceFactory + */ + private $shippingFactory; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteria; + + /** + * @var QuoteIdMaskFactory + */ + private $maskFactory; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->management = $objectManager->get(GuestShippingInformationManagementInterface::class); + $this->cartRepo = $objectManager->get(CartRepositoryInterface::class); + $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); + $this->shippingFactory = $objectManager->get(ShippingInformationInterfaceFactory::class); + $this->searchCriteria = $objectManager->get(SearchCriteriaBuilder::class); + $this->maskFactory = $objectManager->get(QuoteIdMaskFactory::class); + } + + /** + * Test using another address for quote. + * + * @param bool $swapShipping Whether to swap shipping or billing addresses. + * @return void + * + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + * @dataProvider differentAddressesDataProvider + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Unable to save shipping information. Please check input data. + */ + public function testDifferentAddresses(bool $swapShipping) + { + $carts = $this->cartRepo->getList( + $this->searchCriteria->addFilter('reserved_order_id', 'test01')->create() + )->getItems(); + $cart = array_pop($carts); + $otherCustomer = $this->customerRepo->get('customer_with_addresses@test.com'); + $otherAddresses = $otherCustomer->getAddresses(); + $otherAddress = array_pop($otherAddresses); + + //Setting invalid IDs. + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $cart->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $cart->getBillingAddress(); + if ($swapShipping) { + $address = $shippingAddress; + } else { + $address = $billingAddress; + } + $address->setCustomerAddressId($otherAddress->getId()); + $address->setCustomerId($otherCustomer->getId()); + $address->setId(null); + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + /** @var QuoteIdMask $idMask */ + $idMask = $this->maskFactory->create(); + $idMask->load($cart->getId(), 'quote_id'); + $this->management->saveAddressInformation($idMask->getMaskedId(), $shippingInformation); + } + + /** + * @return array + */ + public function differentAddressesDataProvider(): array + { + return [ + 'Shipping address swap' => [true], + 'Billing address swap' => [false], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php new file mode 100644 index 0000000000000..ff795a73fec35 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Api; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test ShippingInformationManagement API. + */ +class ShippingInformationManagementTest extends TestCase +{ + /** + * @var ShippingInformationManagementInterface + */ + private $management; + + /** + * @var CartRepositoryInterface + */ + private $cartRepo; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepo; + + /** + * @var ShippingInformationInterfaceFactory + */ + private $shippingFactory; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->management = $objectManager->get(ShippingInformationManagementInterface::class); + $this->cartRepo = $objectManager->get(CartRepositoryInterface::class); + $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); + $this->shippingFactory = $objectManager->get(ShippingInformationInterfaceFactory::class); + } + + /** + * Test using another address for quote. + * + * @param bool $swapShipping Whether to swap shipping or billing addresses. + * @return void + * + * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + * @dataProvider differentAddressesDataProvider + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Unable to save shipping information. Please check input data. + */ + public function testDifferentAddresses(bool $swapShipping) + { + $cart = $this->cartRepo->getForCustomer(1); + $otherCustomer = $this->customerRepo->get('customer_with_addresses@test.com'); + $otherAddresses = $otherCustomer->getAddresses(); + $otherAddress = array_pop($otherAddresses); + + //Setting invalid IDs. + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $cart->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $cart->getBillingAddress(); + if ($swapShipping) { + $address = $shippingAddress; + } else { + $address = $billingAddress; + } + $address->setCustomerAddressId($otherAddress->getId()); + $address->setCustomerId($otherCustomer->getId()); + $address->setId(null); + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + $this->management->saveAddressInformation($cart->getId(), $shippingInformation); + } + + /** + * @return array + */ + public function differentAddressesDataProvider(): array + { + return [ + 'Shipping address swap' => [true], + 'Billing address swap' => [false], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php index 661593b65adca..ec450d3f2fdda 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php @@ -28,13 +28,14 @@ public function testExecute() 'remove' => 0, 'coupon_code' => 'test' ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($inputData); $this->dispatch( 'checkout/cart/couponPost/' ); $this->assertSessionMessages( - $this->equalTo(['The coupon code "test" is not valid.']), + $this->equalTo(['The coupon code "test" is not valid.']), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } @@ -65,7 +66,7 @@ public function testAddingValidCoupon() ); $this->assertSessionMessages( - $this->equalTo(['You used coupon code "' . $couponCode . '".']), + $this->equalTo(['You used coupon code "' . $couponCode . '".']), \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS ); } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php index 068a9c3529c15..d2e93f7c94ff4 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php @@ -22,6 +22,7 @@ use Magento\Customer\Model\Session as CustomerSession; use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; use Magento\Sales\Model\ResourceModel\Order\Item\Collection as OrderItemCollection; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -286,7 +287,8 @@ private function getQuote($reservedOrderId) * Gets \Magento\Quote\Model\Quote\Item from \Magento\Quote\Model\Quote by product id * * @param \Magento\Quote\Model\Quote $quote - * @param $productId + * @param string|int $productId + * * @return \Magento\Quote\Model\Quote\Item|null */ private function _getQuoteItemIdByProductId($quote, $productId) @@ -321,6 +323,7 @@ public function testAddToCartSimpleProduct($area, $expectedPrice) 'isAjax' => 1 ]; \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea($area); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $quote = $this->_objectManager->create(\Magento\Checkout\Model\Cart::class); @@ -367,6 +370,7 @@ public function testMessageAtAddToCartWithRedirect() ]; \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('checkout/cart/add'); @@ -402,6 +406,7 @@ public function testMessageAtAddToCartWithoutRedirect() ]; \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('checkout/cart/add'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php index 43108dbca1f5e..85dede0d84c2d 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php @@ -21,6 +21,7 @@ class ResetQuoteAddressesTest extends \PHPUnit\Framework\TestCase { /** * @magentoDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + * @magentoAppArea frontend * * @return void */ diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php index 79d4a05aba3c4..994d4d1412b05 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php @@ -58,6 +58,7 @@ public function testExecute() $this->mediaDirectory->getRelativePath($fullDirectoryPath . $directoryName) ); $this->model->getRequest()->setParams(['node' => $this->imagesHelper->idEncode($directoryName)]); + $this->model->getRequest()->setMethod('POST'); $this->model->execute(); $this->assertFalse( $this->mediaDirectory->isExist( diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php index 629d997a64f87..6b1f8fc717c2d 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php @@ -77,6 +77,7 @@ public function testExecute() $this->mediaDirectory->create($this->mediaDirectory->getRelativePath($fullDirectoryPath)); $this->model->getRequest()->setParams(['type' => 'image/png']); + $this->model->getRequest()->setMethod('POST'); $this->model->getStorage()->getSession()->setCurrentPath($fullDirectoryPath); $this->model->execute(); $this->assertTrue( @@ -101,6 +102,7 @@ public function testExecuteWithLinkedMedia() $fullDirectoryPath = $this->filesystem->getDirectoryRead(DirectoryList::PUB) ->getAbsolutePath() . DIRECTORY_SEPARATOR . $directoryName; $wysiwygDir = $this->mediaDirectory->getAbsolutePath() . '/wysiwyg'; + $this->model->getRequest()->setMethod('POST'); $this->model->getRequest()->setParams(['type' => 'image/png']); $this->model->getStorage()->getSession()->setCurrentPath($wysiwygDir); $this->model->execute(); diff --git a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php index a1998c6c89536..782d8dadcc1e8 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigSetCommandTest.php @@ -6,6 +6,8 @@ namespace Magento\Config\Console\Command; use Magento\Config\Model\Config\Backend\Admin\Custom; +use Magento\Config\Model\Config\Structure\Converter; +use Magento\Config\Model\Config\Structure\Data as StructureData; use Magento\Directory\Model\Currency; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -90,6 +92,8 @@ protected function setUp() { Bootstrap::getInstance()->reinitialize(); $this->objectManager = Bootstrap::getObjectManager(); + $this->extendSystemStructure(); + $this->scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); $this->reader = $this->objectManager->get(FileReader::class); $this->filesystem = $this->objectManager->get(Filesystem::class); @@ -122,6 +126,21 @@ protected function tearDown() $this->appConfig->reinit(); } + /** + * Add test system structure to main system structure + * + * @return void + */ + private function extendSystemStructure() + { + $document = new \DOMDocument(); + $document->load(__DIR__ . '/../../_files/system.xml'); + $converter = $this->objectManager->get(Converter::class); + $systemConfig = $converter->convert($document); + $structureData = $this->objectManager->get(StructureData::class); + $structureData->merge($systemConfig); + } + /** * @return array */ @@ -190,6 +209,8 @@ public function runLockDataProvider() ['general/region/display_all', '1'], ['general/region/state_required', 'BR,FR', ScopeInterface::SCOPE_WEBSITE, 'base'], ['admin/security/use_form_key', '0'], + ['general/group/subgroup/field', 'default_value'], + ['general/group/subgroup/field', 'website_value', ScopeInterface::SCOPE_WEBSITE, 'base'], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Config/Controller/Adminhtml/System/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Config/Controller/Adminhtml/System/ConfigTest.php index 9ad99745d572f..5170a4b8a4dd6 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Controller/Adminhtml/System/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Controller/Adminhtml/System/ConfigTest.php @@ -9,6 +9,7 @@ namespace Magento\Config\Controller\Adminhtml\System; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml @@ -22,6 +23,8 @@ public function testEditAction() } /** + * Test redirect after changing base URL. + * * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ @@ -31,20 +34,22 @@ public function testChangeBaseUrl() $newHost = 'm2test123.loc'; $request = $this->getRequest(); $request->setPostValue( - ['groups' => - ['unsecure' => - ['fields' => - ['base_url' => - ['value' => 'http://' . $newHost . '/'] + [ + 'groups' => + ['unsecure' => + ['fields' => + ['base_url' => + ['value' => 'http://' . $newHost . '/'] + ] ] - ] - ], - 'config_state' => - ['web_unsecure' => 1] + ], + 'config_state' => ['web_unsecure' => 1] ] )->setParam( 'section', 'web' + )->setMethod( + HttpRequest::METHOD_POST ); $this->dispatch('backend/admin/system_config/save'); @@ -62,14 +67,16 @@ public function testChangeBaseUrl() } /** - * Reset test framework default base url + * Reset test framework default base url. + * + * @param string $defaultHost */ protected function resetBaseUrl($defaultHost) { $baseUrlData = [ 'section' => 'web', - 'website' => NULL, - 'store' => NULL, + 'website' => null, + 'store' => null, 'groups' => [ 'unsecure' => [ 'fields' => [ diff --git a/dev/tests/integration/testsuite/Magento/Config/_files/system.xml b/dev/tests/integration/testsuite/Magento/Config/_files/system.xml new file mode 100644 index 0000000000000..f0063a3c0bf7f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/_files/system.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="general"> + <group id="group"> + <group id="subgroup"> + <field id="field" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Label</label> + </field> + </group> + </group> + </section> + </system> +</config> diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/ProductTest.php index 4254a6ce9c71d..b71507ae43f9f 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/ProductTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Registry; use Magento\TestFramework\ObjectManager; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml @@ -23,6 +24,7 @@ public function testSaveActionAssociatedProductIds() { $associatedProductIds = ['3', '14', '15', '92']; $associatedProductIdsJSON = json_encode($associatedProductIds); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'id' => 1, diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/CartTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/CartTest.php index f9776b2264ff3..0d93d3ad4f4ae 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/CartTest.php @@ -9,6 +9,8 @@ */ namespace Magento\ConfigurableProduct\Controller; +use Magento\Framework\App\Request\Http as HttpRequest; + class CartTest extends \Magento\TestFramework\TestCase\AbstractController { /** @@ -85,13 +87,14 @@ public function testExecuteForConfigurableLastOption() 'remove' => 0, 'coupon_code' => 'test' ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($inputData); $this->dispatch( 'checkout/cart/couponPost/' ); $this->assertSessionMessages( - $this->equalTo(['The coupon code "test" is not valid.']), + $this->equalTo(['The coupon code "test" is not valid.']), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerWithDimensionTest.php index f08f0a4543ea3..140df500472b3 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerWithDimensionTest.php @@ -16,7 +16,7 @@ /** * @magentoDbIsolation disabled * @group indexer_dimension - * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group + * @--magentoIndexerDimensionMode catalog_product_price website_and_customer_group */ class SpecialPriceIndexerWithDimensionTest extends \PHPUnit\Framework\TestCase { @@ -51,6 +51,9 @@ protected function setUp() */ public function testFullReindexIfChildHasSpecialPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $specialPrice = 2; /** @var Product $childProduct */ $childProduct = $this->productRepository->get('simple_10', true); @@ -88,6 +91,9 @@ public function testFullReindexIfChildHasSpecialPrice() */ public function testOnSaveIndexationIfChildHasSpecialPrice() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $specialPrice = 2; /** @var Product $childProduct */ $childProduct = $this->productRepository->get('simple_10', true); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox/RenderingBasedOnIsProductListFlagWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox/RenderingBasedOnIsProductListFlagWithDimensionTest.php index bddbb38e9f019..214dcafc9f686 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox/RenderingBasedOnIsProductListFlagWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox/RenderingBasedOnIsProductListFlagWithDimensionTest.php @@ -16,7 +16,7 @@ /** * @magentoDbIsolation disabled - * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group + * @--magentoIndexerDimensionMode catalog_product_price website_and_customer_group * @group indexer_dimension * Test price rendering according to is_product_list flag for Configurable product */ @@ -82,6 +82,9 @@ protected function setUp() */ public function testRenderingByDefault() { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $html = $this->finalPriceBox->toHtml(); self::assertContains('5.99', $html); $this->assertGreaterThanOrEqual( @@ -117,6 +120,9 @@ public function testRenderingByDefault() */ public function testRenderingAccordingToIsProductListFlag($flag, $count) { + $this->markTestSkipped( + 'Skipped because of MAGETWO-99136' + ); $this->finalPriceBox->setData('is_product_list', $flag); $html = $this->finalPriceBox->toHtml(); self::assertContains('5.99', $html); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php index 234f0aae6a3ea..63858e91b64f2 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php @@ -5,7 +5,19 @@ */ namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\Data; -class AssociatedProductsTest extends \PHPUnit\Framework\TestCase +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\ConfigurablePanel; +use Magento\Framework\App\RequestInterface; +use PHPUnit\Framework\TestCase; + +/** + * AssociatedProductsTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AssociatedProductsTest extends TestCase { /** * @var \Magento\Framework\ObjectManagerInterface $objectManager @@ -17,6 +29,9 @@ class AssociatedProductsTest extends \PHPUnit\Framework\TestCase */ private $registry; + /** + * @inheritdoc + */ public function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -64,6 +79,53 @@ public function testGetProductMatrix($interfaceLocale) } } + /** + * Tests configurable product won't appear in product listing. + * + * Tests configurable product won't appear in configurable associated product listing if its options attribute + * is not filterable in grid. + * + * @return void + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea adminhtml + */ + public function testAddManuallyConfigurationsWithNotFilterableInGridAttribute() + { + /** @var RequestInterface $request */ + $request = $this->objectManager->get(RequestInterface::class); + $request->setParams([ + FilterModifier::FILTER_MODIFIER => [ + 'test_configurable' => [ + 'condition_type' => 'notnull', + ], + ], + 'attributes_codes' => [ + 'test_configurable' + ], + ]); + $context = $this->objectManager->create(ContextInterface::class, ['request' => $request]); + /** @var UiComponentFactory $uiComponentFactory */ + $uiComponentFactory = $this->objectManager->get(UiComponentFactory::class); + $uiComponent = $uiComponentFactory->create( + ConfigurablePanel::ASSOCIATED_PRODUCT_LISTING, + null, + ['context' => $context] + ); + + foreach ($uiComponent->getChildComponents() as $childUiComponent) { + $childUiComponent->prepare(); + } + + $dataSource = $uiComponent->getDataSourceData(); + $skus = array_column($dataSource['data']['items'], 'sku'); + + $this->assertNotContains( + 'configurable', + $skus, + 'Only products with specified attribute should be in product list' + ); + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php index c9f2ffad67644..fefd1a7b250c3 100644 --- a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php +++ b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php @@ -5,6 +5,8 @@ */ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; +use Magento\Framework\App\Request\Http as HttpRequest; + class SaveRatesTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -43,6 +45,7 @@ public function testSaveAction() $rate = 1.0000; $request = $this->getRequest(); + $request->setMethod(HttpRequest::METHOD_POST); $request->setPostValue( 'rate', [ @@ -75,6 +78,7 @@ public function testSaveWithWarningAction() $rate = '0'; $request = $this->getRequest(); + $request->setMethod(HttpRequest::METHOD_POST); $request->setPostValue( 'rate', [ diff --git a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/SaveTest.php b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/SaveTest.php index 5217c3576a51d..2929f137be89f 100644 --- a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/SaveTest.php @@ -5,10 +5,16 @@ */ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol; +use Magento\Framework\App\Request\Http as HttpRequest; + class SaveTest extends \Magento\TestFramework\TestCase\AbstractBackendController { /** - * Test save action + * Test save action. + * + * @param string $currencyCode + * @param string $inputCurrencySymbol + * @param string $outputCurrencySymbol * * @magentoConfigFixture currency/options/allow USD * @magentoDbIsolation enabled @@ -31,6 +37,7 @@ public function testSaveAction($currencyCode, $inputCurrencySymbol, $outputCurre $currencyCode => $inputCurrencySymbol, ] ); + $request->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/admin/system_currencysymbol/save'); $this->assertRedirect(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index 4fed6c84ab09d..c169272b133bc 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -27,6 +27,7 @@ use Magento\Framework\Stdlib\CookieManagerInterface; use Magento\Theme\Controller\Result\MessagePlugin; use Zend\Stdlib\Parameters; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -111,10 +112,8 @@ public function testForgotPasswordEmailMessageWithSpecialCharacters() { $email = 'customer@example.com'; - $this->getRequest() - ->setPostValue([ - 'email' => $email, - ]); + $this->getRequest()->setPostValue(['email' => $email]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('customer/account/forgotPasswordPost'); $this->assertRedirect($this->stringContains('customer/account/')); @@ -232,6 +231,7 @@ public function testConfirmActionAlreadyActive() public function testNoFormKeyCreatePostAction() { $this->fillRequestWithAccountData(); + $this->getRequest()->setPostValue('form_key', null); $this->dispatch('customer/account/createPost'); $this->assertNull($this->getCustomerByEmail('test1@email.com')); @@ -279,8 +279,7 @@ public function testWithConfirmCreatePostAction() */ public function testExistingEmailCreatePostAction() { - $this->fillRequestWithAccountDataAndFormKey(); - $this->getRequest()->setParam('email', 'customer@example.com'); + $this->fillRequestWithAccountDataAndFormKey('customer@example.com'); $this->dispatch('customer/account/createPost'); $this->assertRedirect($this->stringContains('customer/account/create/')); $this->assertSessionMessages( @@ -339,10 +338,8 @@ public function testForgotPasswordPostAction() { $email = 'customer@example.com'; - $this->getRequest() - ->setPostValue([ - 'email' => $email, - ]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue(['email' => $email]); $this->dispatch('customer/account/forgotPasswordPost'); $this->assertRedirect($this->stringContains('customer/account/')); @@ -362,6 +359,7 @@ public function testForgotPasswordPostAction() */ public function testForgotPasswordPostWithBadEmailAction() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest() ->setPostValue([ 'email' => 'bad@email', @@ -383,6 +381,7 @@ public function testResetPasswordPostNoTokenAction() $this->getRequest() ->setParam('id', 1) ->setParam('token', '8ed8677e6c79e68b94e61658bd756ea5') + ->setMethod('POST') ->setPostValue([ 'password' => 'new-password', 'password_confirmation' => 'new-password', @@ -515,18 +514,19 @@ public function testChangePasswordEditPostAction() $this->login(1); $this->getRequest() ->setMethod('POST') - ->setPostValue([ - 'form_key' => $this->_objectManager->get( - FormKey::class)->getFormKey(), - 'firstname' => 'John', - 'lastname' => 'Doe', - 'email' => 'johndoe@email.com', - 'change_password' => 1, - 'change_email' => 1, - 'current_password' => 'password', - 'password' => 'new-Password1', - 'password_confirmation' => 'new-Password1', - ]); + ->setPostValue( + [ + 'form_key' => $this->_objectManager->get(FormKey::class)->getFormKey(), + 'firstname' => 'John', + 'lastname' => 'Doe', + 'email' => 'johndoe@email.com', + 'change_password' => 1, + 'change_email' => 1, + 'current_password' => 'password', + 'password' => 'new-Password1', + 'password_confirmation' => 'new-Password1', + ] + ); $this->dispatch('customer/account/editPost'); @@ -550,14 +550,16 @@ public function testMissingDataEditPostAction() $this->login(1); $this->getRequest() ->setMethod('POST') - ->setPostValue([ - 'form_key' => $this->_objectManager->get(FormKey::class)->getFormKey(), - 'firstname' => 'John', - 'lastname' => 'Doe', - 'change_email' => 1, - 'current_password' => 'password', - 'email' => 'bad-email', - ]); + ->setPostValue( + [ + 'form_key' => $this->_objectManager->get(FormKey::class)->getFormKey(), + 'firstname' => 'John', + 'lastname' => 'Doe', + 'change_email' => 1, + 'current_password' => 'password', + 'email' => 'bad-email', + ] + ); $this->dispatch('customer/account/editPost'); @@ -576,17 +578,18 @@ public function testWrongPasswordEditPostAction() $this->login(1); $this->getRequest() ->setMethod('POST') - ->setPostValue([ - 'form_key' => $this->_objectManager->get( - FormKey::class)->getFormKey(), - 'firstname' => 'John', - 'lastname' => 'Doe', - 'email' => 'johndoe@email.com', - 'change_password' => 1, - 'current_password' => 'wrong-password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]); + ->setPostValue( + [ + 'form_key' => $this->_objectManager->get(FormKey::class)->getFormKey(), + 'firstname' => 'John', + 'lastname' => 'Doe', + 'email' => 'johndoe@email.com', + 'change_password' => 1, + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ] + ); $this->dispatch('customer/account/editPost'); @@ -607,8 +610,7 @@ public function testWrongConfirmationEditPostAction() $this->getRequest() ->setMethod('POST') ->setPostValue([ - 'form_key' => $this->_objectManager->get( - FormKey::class)->getFormKey(), + 'form_key' => $this->_objectManager->get(FormKey::class)->getFormKey(), 'firstname' => 'John', 'lastname' => 'Doe', 'email' => 'johndoe@email.com', @@ -640,19 +642,18 @@ public function testWrongConfirmationEditPostAction() public function testLoginPostRedirect($redirectDashboard, string $redirectUrl) { if (isset($redirectDashboard)) { - $this->_objectManager->get(ScopeConfigInterface::class)->setValue('customer/startup/redirect_dashboard', $redirectDashboard); + $this->_objectManager->get(ScopeConfigInterface::class)->setValue( + 'customer/startup/redirect_dashboard', + $redirectDashboard + ); } - $this->_objectManager->get(Redirect::class)->setRedirectCookie('test'); - $configValue = $this->_objectManager->create(Value::class); $configValue->load('web/unsecure/base_url', 'path'); $baseUrl = $configValue->getValue() ?: 'http://localhost/'; - $request = $this->prepareRequest(); $app = $this->_objectManager->create(Http::class, ['_request' => $request]); $response = $app->launch(); - $this->assertResponseRedirect($response, $baseUrl . $redirectUrl); $this->assertTrue($this->_objectManager->get(Session::class)->isLoggedIn()); } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AddressTest.php index ddf23e1b6ea98..484725346af64 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AddressTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Model\CustomerRegistry; use Magento\Framework\Data\Form\FormKey; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; class AddressTest extends \Magento\TestFramework\TestCase\AbstractController { @@ -18,6 +19,9 @@ class AddressTest extends \Magento\TestFramework\TestCase\AbstractController /** @var FormKey */ private $formKey; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); @@ -165,7 +169,7 @@ public function testFailedFormPostAction() public function testDeleteAction() { $this->getRequest()->setParam('id', 1); - $this->getRequest()->setParam('form_key', $this->formKey->getFormKey()); + $this->getRequest()->setParam('form_key', $this->formKey->getFormKey())->setMethod(HttpRequest::METHOD_POST); // we are overwriting the address coming from the fixture $this->dispatch('customer/address/delete'); @@ -183,13 +187,13 @@ public function testDeleteAction() public function testWrongAddressDeleteAction() { $this->getRequest()->setParam('id', 555); - $this->getRequest()->setParam('form_key', $this->formKey->getFormKey()); + $this->getRequest()->setParam('form_key', $this->formKey->getFormKey())->setMethod(HttpRequest::METHOD_POST); // we are overwriting the address coming from the fixture $this->dispatch('customer/address/delete'); $this->assertRedirect($this->stringContains('customer/address/index')); $this->assertSessionMessages( - $this->equalTo(['We can\'t delete the address right now.']), + $this->equalTo(['We can't delete the address right now.']), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php index 094cc46d42867..1cc421fd2973d 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php @@ -7,7 +7,7 @@ use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml @@ -26,6 +26,9 @@ class GroupTest extends \Magento\TestFramework\TestCase\AbstractBackendControlle /** @var \Magento\Customer\Api\GroupRepositoryInterface */ private $groupRepository; + /** + * @inheritDoc + */ public function setUp() { parent::setUp(); @@ -34,12 +37,9 @@ public function setUp() $this->groupRepository = $objectManager->get(\Magento\Customer\Api\GroupRepositoryInterface::class); } - public function tearDown() - { - parent::tearDown(); - //$this->session->unsCustomerGroupData(); - } - + /** + * Test new group form. + */ public function testNewActionNoCustomerGroupDataInSession() { $this->dispatch('backend/customer/group/new'); @@ -50,6 +50,9 @@ public function testNewActionNoCustomerGroupDataInSession() $this->assertContains($expected, $responseBody); } + /** + * Test form filling with data in session. + */ public function testNewActionWithCustomerGroupDataInSession() { /** @var \Magento\Customer\Api\Data\GroupInterfaceFactory $customerGroupFactory */ @@ -77,36 +80,27 @@ public function testNewActionWithCustomerGroupDataInSession() } /** + * Test calling delete without an ID. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testDeleteActionNoGroupId() { - /** @var FormKey $formKey */ - $formKey = $this->_objectManager->get(FormKey::class); - - $this->getRequest()->setMethod('POST'); - $this->getRequest()->setParam('form_key', $formKey->getFormKey()); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/delete'); $this->assertRedirect($this->stringStartsWith(self::BASE_CONTROLLER_URL)); } /** + * Test deleting a group. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testDeleteActionExistingGroup() { $groupId = $this->findGroupIdWithCode(self::CUSTOMER_GROUP_CODE); - - /** @var FormKey $formKey */ - $formKey = $this->_objectManager->get(FormKey::class); - - $this->getRequest()->setMethod('POST'); - $this->getRequest()->setParams( - [ - 'id' => $groupId, - 'form_key' => $formKey->getFormKey() - ] - ); + $this->getRequest()->setParam('id', $groupId); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/delete'); /** @@ -120,20 +114,14 @@ public function testDeleteActionExistingGroup() } /** + * Tet deleting with wrong ID. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testDeleteActionNonExistingGroupId() { - /** @var FormKey $formKey */ - $formKey = $this->_objectManager->get(FormKey::class); - - $this->getRequest()->setMethod('POST'); - $this->getRequest()->setParams( - [ - 'id' => 10000, - 'form_key' => $formKey->getFormKey() - ] - ); + $this->getRequest()->setParam('id', 10000); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/delete'); /** @@ -147,6 +135,8 @@ public function testDeleteActionNonExistingGroupId() } /** + * Test saving a valid group. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testSaveActionExistingGroup() @@ -155,6 +145,7 @@ public function testSaveActionExistingGroup() $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); $this->getRequest()->setParam('id', $groupId); $this->getRequest()->setParam('code', self::CUSTOMER_GROUP_CODE); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); @@ -186,9 +177,13 @@ public function testSaveActionExistingGroup() ); } + /** + * Test saving an invalid group. + */ public function testSaveActionCreateNewGroupWithoutCode() { $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); @@ -198,19 +193,26 @@ public function testSaveActionCreateNewGroupWithoutCode() ); } + /** + * Test saving an empty group. + */ public function testSaveActionForwardNewCreateNewGroup() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); $responseBody = $this->getResponse()->getBody(); $this->assertRegExp('/<h1 class\="page-title">\s*New Customer Group\s*<\/h1>/', $responseBody); } /** + * Test saving an existing group. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testSaveActionForwardNewEditExistingGroup() { $groupId = $this->findGroupIdWithCode(self::CUSTOMER_GROUP_CODE); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('id', $groupId); $this->dispatch('backend/customer/group/save'); @@ -218,10 +220,14 @@ public function testSaveActionForwardNewEditExistingGroup() $this->assertRegExp('/<h1 class\="page-title">\s*' . self::CUSTOMER_GROUP_CODE . '\s*<\/h1>/', $responseBody); } + /** + * Test using an invalid ID. + */ public function testSaveActionNonExistingGroupId() { $this->getRequest()->setParam('id', 10000); $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); @@ -236,6 +242,8 @@ public function testSaveActionNonExistingGroupId() } /** + * Test using existing code. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testSaveActionNewGroupWithExistingGroupCode() @@ -245,6 +253,7 @@ public function testSaveActionNewGroupWithExistingGroupCode() $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); $this->getRequest()->setParam('code', self::CUSTOMER_GROUP_CODE); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); @@ -257,6 +266,8 @@ public function testSaveActionNewGroupWithExistingGroupCode() } /** + * Test saving an invalid group. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testSaveActionNewGroupWithoutGroupCode() @@ -265,6 +276,7 @@ public function testSaveActionNewGroupWithoutGroupCode() $originalCode = $this->groupRepository->getById($groupId)->getCode(); $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php index 434e24b7d2771..3df07fbd4e1c0 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php @@ -9,6 +9,7 @@ use Magento\Backend\Model\Session; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -31,12 +32,18 @@ class MassAssignGroupTest extends AbstractBackendController */ protected $customerRepository; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); } + /** + * @inheritDoc + */ protected function tearDown() { /** @@ -73,8 +80,8 @@ public function testMassAssignGroupAction() 'form_key' => $formKey->getFormKey() ]; - $this->getRequest()->setParams($params); - $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams($params) + ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massAssignGroup'); $this->assertSessionMessages( self::equalTo(['A total of 1 record(s) were updated.']), @@ -111,8 +118,8 @@ public function testLargeGroupMassAssignGroupAction() 'form_key' => $formKey->getFormKey() ]; - $this->getRequest()->setParams($params); - $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams($params) + ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massAssignGroup'); $this->assertSessionMessages( self::equalTo(['A total of 5 record(s) were updated.']), @@ -141,9 +148,7 @@ public function testMassAssignGroupActionNoCustomerIds() 'namespace' => 'customer_listing', 'form_key' => $formKey->getFormKey() ]; - - $this->getRequest()->setParams($params); - $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams($params)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massAssignGroup'); $this->assertSessionMessages( $this->equalTo(['Please select item(s).']), diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php index 96e993932cb18..dc192c3c8681d 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\Constraint\Constraint; use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\TestFramework\TestCase\AbstractBackendController; /** @@ -32,12 +33,18 @@ class MassDeleteTest extends AbstractBackendController */ private $baseControllerUrl = 'http://localhost/index.php/backend/customer/index/index'; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); } + /** + * @inheritDoc + */ protected function tearDown() { /** @@ -110,8 +117,7 @@ private function massDeleteAssertions($ids, Constraint $constraint, $messageType 'form_key' => $formKey->getFormKey() ]; - $this->getRequest()->setParams($requestData); - $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams($requestData)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massDelete'); $this->assertSessionMessages( $constraint, diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/ResetPasswordTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/ResetPasswordTest.php index b5ca783d68cf2..eaaba639d49a8 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/ResetPasswordTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/ResetPasswordTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * ResetPassword controller test. * @@ -32,7 +34,7 @@ public function testResetPasswordSuccess() $this->passwordResetRequestEventCreate( \Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST ); - $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setPostValue(['customer_id' => '1'])->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), @@ -55,7 +57,7 @@ public function testResetPasswordMinTimeError() $this->passwordResetRequestEventCreate( \Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST ); - $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setPostValue(['customer_id' => '1'])->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), @@ -78,7 +80,7 @@ public function testResetPasswordLimitError() $this->passwordResetRequestEventCreate( \Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST ); - $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setPostValue(['customer_id' => '1'])->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), @@ -103,7 +105,7 @@ public function testResetPasswordWithSecurityViolationException() $this->passwordResetRequestEventCreate( \Magento\Security\Model\PasswordResetRequestEvent::ADMIN_PASSWORD_RESET_REQUEST ); - $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setPostValue(['customer_id' => '1'])->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php index ccf9c45da8660..4d5c46dff6221 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php @@ -12,6 +12,7 @@ use Magento\Customer\Controller\RegistryConstants; use Magento\Customer\Model\EmailNotification; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml @@ -85,7 +86,7 @@ protected function tearDown() */ public function testSaveActionWithEmptyPostData() { - $this->getRequest()->setPostValue([]); + $this->getRequest()->setPostValue([])->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/save'); $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl)); } @@ -96,43 +97,7 @@ public function testSaveActionWithEmptyPostData() public function testSaveActionWithInvalidFormData() { $post = ['account' => ['middlename' => 'test middlename', 'group_id' => 1]]; - $this->getRequest()->setPostValue($post); - $this->dispatch('backend/customer/index/save'); - /** - * Check that errors was generated and set to session - */ - $this->assertSessionMessages( - $this->logicalNot($this->isEmpty()), - \Magento\Framework\Message\MessageInterface::TYPE_ERROR - ); - /** - * Check that customer data were set to session - */ - $this->assertEquals( - $post, - $this->objectManager->get(\Magento\Backend\Model\Session::class)->getCustomerFormData() - ); - $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl . 'new')); - } - - /** - * @magentoDbIsolation enabled - */ - public function testSaveActionWithInvalidCustomerAddressData() - { - $post = [ - 'customer' => [ - 'middlename' => 'test middlename', - 'group_id' => 1, - 'website_id' => 0, - 'firstname' => 'test firstname', - 'lastname' => 'test lastname', - 'email' => 'example@domain.com', - 'default_billing' => '_item1', - ], - 'address' => ['_item1' => []], - ]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/save'); /** * Check that errors was generated and set to session @@ -141,13 +106,13 @@ public function testSaveActionWithInvalidCustomerAddressData() $this->logicalNot($this->isEmpty()), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); + /** @var \Magento\Backend\Model\Session $session */ + $session = $this->objectManager->get(\Magento\Backend\Model\Session::class); /** * Check that customer data were set to session */ - $this->assertEquals( - $post, - $this->objectManager->get(\Magento\Backend\Model\Session::class)->getCustomerFormData() - ); + $this->assertNotEmpty($session->getCustomerFormData()); + $this->assertArraySubset($post, $session->getCustomerFormData()); $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl . 'new')); } @@ -181,7 +146,7 @@ public function testSaveActionWithValidCustomerDataAndValidAddressData() ], ], ]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('back', '1'); // Emulate setting customer data to session in editAction @@ -293,7 +258,7 @@ public function testSaveActionExistingCustomerAndExistingAddressData() ], 'subscription' => '', ]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('id', 1); $this->dispatch('backend/customer/index/save'); @@ -359,7 +324,7 @@ public function testSaveActionExistingCustomerUnsubscribeNewsletter() ], 'subscription' => '0' ]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('id', 1); $this->dispatch('backend/customer/index/save'); @@ -397,7 +362,7 @@ public function testSaveActionExistingCustomerChangeEmail() 'change_email_template', [ 'name' => 'CustomerSupport', - 'email' => 'support@example.com' + 'email' => 'support@example.com', ], $customerId, $newEmail @@ -420,7 +385,7 @@ public function testSaveActionExistingCustomerChangeEmail() 'default_billing' => 1, ] ]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('id', 1); $this->dispatch('backend/customer/index/save'); @@ -447,7 +412,7 @@ public function testInlineEditChangeEmail() 'change_email_template', [ 'name' => 'CustomerSupport', - 'email' => 'support@example.com' + 'email' => 'support@example.com', ], $customerId, $newEmail @@ -467,7 +432,7 @@ public function testInlineEditChangeEmail() ] ]; $this->getRequest()->setParam('ajax', true)->setParam('isAjax', true); - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('id', 1); $this->dispatch('backend/customer/index/inlineEdit'); @@ -493,7 +458,7 @@ public function testSaveActionCoreException() 'password' => 'password', ], ]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/save'); /* * Check that error message is set @@ -502,7 +467,7 @@ public function testSaveActionCoreException() $this->equalTo(['A customer with the same email already exists in an associated website.']), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); - $this->assertEquals( + $this->assertArraySubset( $post, Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->getCustomerFormData() ); @@ -615,8 +580,7 @@ public function testNotExistingCustomerDeleteAction() { $this->getRequest()->setParam('id', 2); $this->getRequest()->setParam('form_key', $this->formKey->getFormKey()); - - $this->getRequest()->setMethod(\Zend\Http\Request::METHOD_POST); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/delete'); $this->assertRedirect($this->stringContains('customer/index')); @@ -693,7 +657,7 @@ public function testValidateCustomerWithAddressSuccess() /** * set customer data */ - $this->getRequest()->setParams($customerData); + $this->getRequest()->setParams($customerData)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/validate'); $body = $this->getResponse()->getBody(); @@ -747,7 +711,7 @@ public function testValidateCustomerWithAddressFailure() /** * set customer data */ - $this->getRequest()->setPostValue($customerData); + $this->getRequest()->setPostValue($customerData)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/validate'); $body = $this->getResponse()->getBody(); @@ -764,6 +728,7 @@ public function testValidateCustomerWithAddressFailure() public function testResetPasswordActionNoCustomerId() { // No customer ID in post, will just get redirected to base + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/resetPassword'); $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl)); } @@ -774,6 +739,7 @@ public function testResetPasswordActionNoCustomerId() public function testResetPasswordActionBadCustomerId() { // Bad customer ID in post, will just get redirected to base + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue(['customer_id' => '789']); $this->dispatch('backend/customer/index/resetPassword'); $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl)); @@ -785,6 +751,7 @@ public function testResetPasswordActionBadCustomerId() public function testResetPasswordActionSuccess() { $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), @@ -825,7 +792,7 @@ protected function prepareEmailMock($occurrenceNumber, $templateId, $sender, $cu 'setTemplateIdentifier', 'setTemplateVars', 'setTemplateOptions', - 'getTransport' + 'getTransport', ] ) ->getMock(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php new file mode 100644 index 0000000000000..415591ac7d990 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Controller; + +use Magento\TestFramework\TestCase\AbstractController; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Framework\Data\Form\FormKey; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Model\Session; +use Psr\Log\LoggerInterface; + +class SendTest extends AbstractController +{ + /** @var AccountManagementInterface */ + private $accountManagement; + + /** @var FormKey */ + private $formKey; + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function setUp() + { + parent::setUp(); + $logger = $this->createMock(LoggerInterface::class); + $session = Bootstrap::getObjectManager()->create( + Session::class, + [$logger] + ); + $this->accountManagement = Bootstrap::getObjectManager()->create(AccountManagementInterface::class); + $this->formKey = Bootstrap::getObjectManager()->create(FormKey::class); + $customer = $this->accountManagement->authenticate('customer@example.com', 'password'); + $session->setCustomerDataAsLoggedIn($customer); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testExecutePost() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'emails' => 'example1@gmail.com, example2@gmail.com, example3@gmail.com' + ] + ); + + $this->dispatch('wishlist/index/send'); + $this->assertRedirect($this->stringContains('wishlist/index/index')); + $this->assertSessionMessages( + $this->equalTo(['Your wish list has been shared.']), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php index 2e132d27f5cb1..507150029c0c6 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php @@ -14,6 +14,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\ExpiredException; use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; /** @@ -53,6 +54,9 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Framework\Api\ExtensibleDataObjectConverter */ private $extensibleDataObjectConverter; + /** @var StoreManagerInterface */ + private $storeManager; + /** @var \Magento\Framework\Api\DataObjectHelper */ protected $dataObjectHelper; @@ -114,6 +118,9 @@ protected function setUp() $this->extensibleDataObjectConverter = $this->objectManager ->create(\Magento\Framework\Api\ExtensibleDataObjectConverter::class); + + $this->storeManager = $this->objectManager + ->create(StoreManagerInterface::class); } /** @@ -1028,4 +1035,42 @@ protected function setResetPasswordData( $customerModel->setRpTokenCreatedAt(date($date)); $customerModel->save(); } + + /** + * Customer has two addresses one of it is allowed in website and second is not + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoDataFixture Magento/Store/_files/websites_different_countries.php + * @magentoConfigFixture fixture_second_store_store general/country/allow UA + * @return void + */ + public function testCreateNewCustomerWithPasswordHashWithNotAllowedCountry() + { + $customerId = 1; + $allowedCountryIdForSecondWebsite = 'UA'; + $store = $this->storeManager->getStore('fixture_second_store'); + $customerData = $this->customerRepository->getById($customerId); + $customerData->getAddresses()[1]->setRegion(null)->setCountryId($allowedCountryIdForSecondWebsite) + ->setRegionId(null); + $customerData->setStoreId($store->getId())->setWebsiteId($store->getWebsiteId())->setId(null); + $encryptor = $this->objectManager->get(\Magento\Framework\Encryption\EncryptorInterface::class); + /** @var \Magento\Framework\Math\Random $mathRandom */ + $password = $this->objectManager->get(\Magento\Framework\Math\Random::class)->getRandomString(8); + $passwordHash = $encryptor->getHash($password, true); + $savedCustomer = $this->accountManagement->createAccountWithPasswordHash( + $customerData, + $passwordHash + ); + $this->assertCount( + 1, + $savedCustomer->getAddresses(), + 'The wrong address quantity was saved' + ); + $this->assertSame( + 'UA', + $savedCustomer->getAddresses()[0]->getCountryId(), + 'The address with the disallowed country was saved' + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php new file mode 100644 index 0000000000000..a07010249319c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\CustomerRegistry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\Address; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Model\AddressRegistry; + +$objectManager = Bootstrap::getObjectManager(); +//Creating customer +/** @var $repository CustomerRepositoryInterface */ +$repository = $objectManager->create(CustomerRepositoryInterface::class); +/** @var Customer $customer */ +$customer = $objectManager->create(Customer::class); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +$customer->setWebsiteId(1) + ->setEmail('customer_with_addresses@test.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + +$customer->isObjectNew(true); +$customer->save(); +$customerRegistry->remove($customer->getId()); + +//Creating address +/** @var Address $customerAddress */ +$customerAddress = $objectManager->create(Address::class); +$customerAddress->isObjectNew(true); +$customerAddress->setData( + [ + 'attribute_set_id' => 2, + 'telephone' => 3468676, + 'postcode' => 75477, + 'country_id' => 'US', + 'city' => 'CityM', + 'company' => 'CompanyName', + 'street' => 'CustomerAddress1', + 'lastname' => 'Smith', + 'firstname' => 'John', + 'parent_id' => $customer->getId(), + 'region_id' => 1, + ] +); +$customerAddress->save(); +/** @var AddressRepositoryInterface $addressRepository */ +$addressRepository = $objectManager->get(AddressRepositoryInterface::class); +$customerAddress = $addressRepository->getById($customerAddress->getId()); +$customerAddress->setCustomerId($customer->getId()); +$customerAddress->isDefaultBilling(true); +$customerAddress->setIsDefaultShipping(true); +$customerAddress = $addressRepository->save($customerAddress); +$customerRegistry->remove($customerAddress->getCustomerId()); +/** @var AddressRegistry $addressRegistry */ +$addressRegistry = $objectManager->get(AddressRegistry::class); +$addressRegistry->remove($customerAddress->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php new file mode 100644 index 0000000000000..c3acf62cddefa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var CustomerRepositoryInterface $customerRepo */ +$customerRepo = $objectManager->get(CustomerRepositoryInterface::class); +try { + $customer = $customerRepo->get('customer_with_addresses@test.com'); + $customerRepo->delete($customer); +} catch (NoSuchEntityException $exception) { + //Already deleted +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_rollback.php new file mode 100644 index 0000000000000..cde7569cc2467 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require 'customer_rollback.php'; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); +try { + $customer = $customerRepository->get('customer_two@example.com'); + $customerRepository->delete($customer); +} catch (NoSuchEntityException $e) { + /** Tests which are wrapped with MySQL transaction clear all data by transaction rollback. */ +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php new file mode 100644 index 0000000000000..dd1f917e34d5e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -0,0 +1,252 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Dhl\Model; + +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Framework\Simplexml\Element; +use Magento\Shipping\Model\Tracking\Result\Status; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Test DHL Shipping Method. + */ +class CarrierTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Carrier + */ + private $dhlCarrier; + + /** + * @var ZendClient|MockObject + */ + private $httpClientMock; + + /** + * @var \Zend_Http_Response|MockObject + */ + private $httpResponseMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->dhlCarrier = $objectManager->create( + Carrier::class, + ['httpClientFactory' => $this->getHttpClientFactory()] + ); + } + + /** + * @magentoDbIsolation enabled + * + * @magentoConfigFixture default_store carriers/dhl/id CustomerSiteID + * @magentoConfigFixture default_store carriers/dhl/password CustomerPassword + * + * @param array $trackingNumbers + * @param string $responseXml + * @param array $expectedTrackingData + * @param string $expectedRequestXml + * @dataProvider getTrackingDataProvider + * + * @return void + */ + public function testGetTracking( + array $trackingNumbers, + string $responseXml, + array $expectedTrackingData, + string $expectedRequestXml = '' + ) { + $this->httpResponseMock->method('getBody') + ->willReturn($responseXml); + $trackingResult = $this->dhlCarrier->getTracking($trackingNumbers); + $this->assertTrackingResult($expectedTrackingData, $trackingResult->getAllTrackings()); + if ($expectedRequestXml !== '') { + $method = new \ReflectionMethod($this->httpClientMock, '_prepareBody'); + $method->setAccessible(true); + $requestXml = $method->invoke($this->httpClientMock); + $this->assertRequest($expectedRequestXml, $requestXml); + } + } + + /** + * Get tracking data provider. + * + * @return array + */ + public function getTrackingDataProvider(): array + { + $expectedMultiAWBRequestXml = file_get_contents(__DIR__ . '/../_files/TrackingRequest_MultipleAWB.xml'); + $multiAWBResponseXml = file_get_contents(__DIR__ . '/../_files/TrackingResponse_MultipleAWB.xml'); + $expectedSingleAWBRequestXml = file_get_contents(__DIR__ . '/../_files/TrackingRequest_SingleAWB.xml'); + $singleAWBResponseXml = file_get_contents(__DIR__ . '/../_files/TrackingResponse_SingleAWB.xml'); + $singleNoDataResponseXml = file_get_contents(__DIR__ . '/../_files/SingleknownTrackResponse-no-data-found.xml'); + $failedResponseXml = file_get_contents(__DIR__ . '/../_files/Track-res-XML-Parse-Err.xml'); + $expectedTrackingDataA = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 4781584780, + 'service' => 'DOCUMENT', + 'progressdetail' => [ + [ + 'activity' => 'SD Shipment information received', + 'deliverydate' => '2017-12-25', + 'deliverytime' => '14:38:00', + 'deliverylocation' => 'BEIJING-CHN [PEK]', + ], + ], + 'weight' => '0.5 K', + ]; + $expectedTrackingDataB = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 4781585060, + 'service' => 'NOT RESTRICTED FOR TRANSPORT,', + 'progressdetail' => [ + [ + 'activity' => 'SD Shipment information received', + 'deliverydate' => '2017-12-24', + 'deliverytime' => '13:35:00', + 'deliverylocation' => 'HONG KONG-HKG [HKG]', + ], + ], + 'weight' => '2.0 K', + ]; + $expectedTrackingDataC = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 5702254250, + 'service' => 'CD', + 'progressdetail' => [ + [ + 'activity' => 'SD Shipment information received', + 'deliverydate' => '2017-12-24', + 'deliverytime' => '04:12:00', + 'deliverylocation' => 'BIRMINGHAM-GBR [BHX]', + ], + ], + 'weight' => '0.12 K', + ]; + $expectedTrackingDataD = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 4781585060, + 'error_message' => __('Unable to retrieve tracking'), + ]; + $expectedTrackingDataE = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 111, + 'error_message' => __( + 'Error #%1 : %2', + '111', + ' Error Parsing incoming request XML + Error: The content of element type + "ShipperReference" must match + "(ReferenceID,ReferenceType?)". at line + 16, column 22' + ), + ]; + + return [ + 'multi-AWB' => [ + ['4781584780', '4781585060', '5702254250'], + $multiAWBResponseXml, + [$expectedTrackingDataA, $expectedTrackingDataB, $expectedTrackingDataC], + $expectedMultiAWBRequestXml, + ], + 'single-AWB' => [ + ['4781585060'], + $singleAWBResponseXml, + [$expectedTrackingDataB], + $expectedSingleAWBRequestXml, + ], + 'single-AWB-no-data' => [ + ['4781585061'], + $singleNoDataResponseXml, + [$expectedTrackingDataD], + ], + 'failed-response' => [ + ['4781585060-failed'], + $failedResponseXml, + [$expectedTrackingDataE], + ], + ]; + } + + /** + * Get mocked Http Client Factory. + * + * @return MockObject + */ + private function getHttpClientFactory(): MockObject + { + $this->httpResponseMock = $this->getMockBuilder(\Zend_Http_Response::class) + ->disableOriginalConstructor() + ->getMock(); + $this->httpClientMock = $this->getMockBuilder(ZendClient::class) + ->disableOriginalConstructor() + ->setMethods(['request']) + ->getMock(); + $this->httpClientMock->method('request') + ->willReturn($this->httpResponseMock); + /** @var ZendClientFactory|MockObject $httpClientFactoryMock */ + $httpClientFactoryMock = $this->getMockBuilder(ZendClientFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $httpClientFactoryMock->method('create') + ->willReturn($this->httpClientMock); + + return $httpClientFactoryMock; + } + + /** + * Assert request. + * + * @param string $expectedRequestXml + * @param string $requestXml + * + * @return void + */ + private function assertRequest(string $expectedRequestXml, string $requestXml) + { + $expectedRequestElement = new Element($expectedRequestXml); + $requestElement = new Element($requestXml); + $requestMessageTime = $requestElement->Request->ServiceHeader->MessageTime->__toString(); + $this->assertRegexp( + "/\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}\+\d{2}\:\d{2}/", + $requestMessageTime + ); + $expectedRequestElement->Request->ServiceHeader->MessageTime = $requestMessageTime; + $messageReference = $requestElement->Request->ServiceHeader->MessageReference->__toString(); + $this->assertStringStartsWith('MAGE_TRCK_', $messageReference); + $this->assertGreaterThanOrEqual(28, strlen($messageReference)); + $this->assertLessThanOrEqual(32, strlen($messageReference)); + $requestElement->Request->ServiceHeader->MessageReference = 'MAGE_TRCK_28TO32_Char_CHECKED'; + $this->assertXmlStringEqualsXmlString($expectedRequestElement->asXML(), $requestElement->asXML()); + } + + /** + * Assert tracking. + * + * @param array|null $expectedTrackingData + * @param Status[]|null $trackingResults + * + * @return void + */ + private function assertTrackingResult($expectedTrackingData, $trackingResults) + { + $ctr = 0; + foreach ($trackingResults as $trackingResult) { + $this->assertEquals($expectedTrackingData[$ctr++], $trackingResult->getData()); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/SingleknownTrackResponse-no-data-found.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/SingleknownTrackResponse-no-data-found.xml new file mode 100644 index 0000000000000..80e7b42e4c534 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/SingleknownTrackResponse-no-data-found.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:TrackingResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingResponse.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:59:34+01:00</MessageTime> + <MessageReference>1234567890123456789012345678</MessageReference> + <SiteID>CustomerSiteID</SiteID> + </ServiceHeader> + </Response> + <AWBInfo> + <AWBNumber>4781585060</AWBNumber> + <Status> + <ActionStatus>No Shipments Found</ActionStatus> + <Condition> + <ConditionCode>209</ConditionCode> + <ConditionData>No Shipments Found for AWBNumber 6017300993</ConditionData> + </Condition> + </Status> + </AWBInfo> + <LanguageCode>String</LanguageCode> +</req:TrackingResponse> + <!-- ServiceInvocationId:20180227125934_5793_74fbd9e1-a8b0-4f6a-a326-26aae979e5f0 --> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/Track-res-XML-Parse-Err.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/Track-res-XML-Parse-Err.xml new file mode 100644 index 0000000000000..a3b4729fb21ae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/Track-res-XML-Parse-Err.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentTrackingErrorResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com track-err-res.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:55:05+01:00</MessageTime> + </ServiceHeader> + <Status> + <ActionStatus>Failure</ActionStatus> + <Condition> + <ConditionCode>111</ConditionCode> + <ConditionData> Error Parsing incoming request XML + Error: The content of element type + "ShipperReference" must match + "(ReferenceID,ReferenceType?)". at line + 16, column 22</ConditionData> + </Condition> + </Status> + </Response> +</req:ShipmentTrackingErrorResponse> + <!-- ServiceInvocationId:20180227125505_5793_2008671c-9292-4790-87b6-b02ccdf913db --> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_MultipleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_MultipleAWB.xml new file mode 100644 index 0000000000000..fefadf2d4ebde --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_MultipleAWB.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:KnownTrackingRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown-1.0.xsd" schemaVersion="1.0"> + <Request> + <ServiceHeader> + <MessageTime>2002-06-25T11:28:56-08:00</MessageTime> + <MessageReference>MAGE_TRCK_28TO32_Char_CHECKED</MessageReference> + <SiteID>CustomerSiteID</SiteID> + <Password>CustomerPassword</Password> + </ServiceHeader> + </Request> + <LanguageCode>EN</LanguageCode> + <AWBNumber>4781584780</AWBNumber> + <AWBNumber>4781585060</AWBNumber> + <AWBNumber>5702254250</AWBNumber> + <LevelOfDetails>ALL_CHECK_POINTS</LevelOfDetails> +</req:KnownTrackingRequest> + + diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_SingleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_SingleAWB.xml new file mode 100644 index 0000000000000..e9968e1464906 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_SingleAWB.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:KnownTrackingRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown-1.0.xsd" schemaVersion="1.0"> + <Request> + <ServiceHeader> + <MessageTime>2002-06-25T11:28:56-08:00</MessageTime> + <MessageReference>MAGE_TRCK_28TO32_Char_CHECKED</MessageReference> + <SiteID>CustomerSiteID</SiteID> + <Password>CustomerPassword</Password> + </ServiceHeader> + </Request> + <LanguageCode>EN</LanguageCode> + <AWBNumber>4781585060</AWBNumber> + <LevelOfDetails>ALL_CHECK_POINTS</LevelOfDetails> +</req:KnownTrackingRequest> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_MultipleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_MultipleAWB.xml new file mode 100644 index 0000000000000..618bbb4de8e78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_MultipleAWB.xml @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:TrackingResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingResponse.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:43:44+01:00</MessageTime> + <MessageReference>1234567890123456789012345678</MessageReference> + <SiteID>CustomerSiteID</SiteID> + </ServiceHeader> + </Response> + <AWBInfo> + <AWBNumber>4781584780</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>PEK</ServiceAreaCode> + <Description>BEIJING-CHN</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>PHL</ServiceAreaCode> + <Description>WEST PHILADELPHIA,PA-USA</Description> + </DestinationServiceArea> + <ShipperName>THE EXP HIGH SCH ATT TO BNU</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>HAVEFORD COLLEGE</ConsigneeName> + <ShipmentDate>2017-12-25T14:38:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>0.5</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>D</GlobalProductCode> + <ShipmentDesc>DOCUMENT</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>BEIJING</City> + <PostalCode>100032</PostalCode> + <CountryCode>CN</CountryCode> + </Shipper> + <Consignee> + <City>HAVERFORD</City> + <DivisionCode>PA</DivisionCode> + <PostalCode>19041</PostalCode> + <CountryCode>US</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>2469</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-25</Date> + <Time>14:38:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>PEK</ServiceAreaCode> + <Description>BEIJING-CHN</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <AWBInfo> + <AWBNumber>4781585060</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </DestinationServiceArea> + <ShipperName>NET-A-PORTER</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>NICOLE LI</ConsigneeName> + <ShipmentDate>2017-12-24T13:35:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>2.0</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>N</GlobalProductCode> + <ShipmentDesc>NOT RESTRICTED FOR TRANSPORT,</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>HONG KONG</City> + <CountryCode>HK</CountryCode> + </Shipper> + <Consignee> + <City>HONG KONG</City> + <DivisionCode>CH</DivisionCode> + <CountryCode>HK</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>1060571</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-24</Date> + <Time>13:35:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <AWBInfo> + <AWBNumber>5702254250</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>BHX</ServiceAreaCode> + <Description>BIRMINGHAM-GBR</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>AOI</ServiceAreaCode> + <Description>ANCONA-ITA</Description> + </DestinationServiceArea> + <ShipperName>AMAZON EU SARL</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>MATTEO LOMBO</ConsigneeName> + <ShipmentDate>2017-12-24T04:12:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>0.12</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>U</GlobalProductCode> + <ShipmentDesc>CD</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>PETERBOROUGH</City> + <PostalCode>PE2 9EN</PostalCode> + <CountryCode>GB</CountryCode> + </Shipper> + <Consignee> + <City>ORTONA</City> + <PostalCode>66026</PostalCode> + <CountryCode>IT</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>DGWYDy4xN_1</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-24</Date> + <Time>04:12:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>BHX</ServiceAreaCode> + <Description>BIRMINGHAM-GBR</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <LanguageCode>en</LanguageCode> +</req:TrackingResponse> + <!-- ServiceInvocationId:20180227124344_5793_23bed3d9-e792-4955-8055-9472b1b41929 --> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_SingleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_SingleAWB.xml new file mode 100644 index 0000000000000..fa31b898b7a1f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_SingleAWB.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:TrackingResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingResponse.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:27:42+01:00</MessageTime> + <MessageReference>1234567890123456789012345678</MessageReference> + <SiteID>CustomerSiteID</SiteID> + </ServiceHeader> + </Response> + <AWBInfo> + <AWBNumber>4781585060</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </DestinationServiceArea> + <ShipperName>NET-A-PORTER</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>NICOLE LI</ConsigneeName> + <ShipmentDate>2017-12-24T13:35:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>2.0</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>N</GlobalProductCode> + <ShipmentDesc>NOT RESTRICTED FOR TRANSPORT,</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>HONG KONG</City> + <CountryCode>HK</CountryCode> + </Shipper> + <Consignee> + <City>HONG KONG</City> + <DivisionCode>CH</DivisionCode> + <CountryCode>HK</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>1060571</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-24</Date> + <Time>13:35:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <LanguageCode>en</LanguageCode> +</req:TrackingResponse> + <!-- ServiceInvocationId:20180227122741_5793_e0f8c40e-5245-4737-ab31-323030366721 --> diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php index b620d9097b4be..10f2749ddace1 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php @@ -56,7 +56,7 @@ protected function setUp() } /** - * Test get currency config for admin and storefront areas. + * Test get currency config for admin, crontab and storefront areas. * * @dataProvider getConfigCurrenciesDataProvider * @magentoDataFixture Magento/Store/_files/store.php @@ -77,7 +77,7 @@ public function testGetConfigCurrencies(string $areaCode, array $expected) $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); $storeManager->setCurrentStore($store->getId()); - if ($areaCode === Area::AREA_ADMINHTML) { + if (in_array($areaCode, [Area::AREA_ADMINHTML, Area::AREA_CRONTAB])) { self::assertEquals($expected['allowed'], $this->currency->getConfigAllowCurrencies()); self::assertEquals($expected['base'], $this->currency->getConfigBaseCurrencies()); self::assertEquals($expected['default'], $this->currency->getConfigDefaultCurrencies()); @@ -118,6 +118,14 @@ public function getConfigCurrenciesDataProvider() 'default' => ['BDT', 'USD'], ], ], + [ + 'areaCode' => Area::AREA_CRONTAB, + 'expected' => [ + 'allowed' => ['BDT', 'BNS', 'BTD', 'EUR', 'USD'], + 'base' => ['BDT', 'USD'], + 'default' => ['BDT', 'USD'], + ], + ], [ 'areaCode' => Area::AREA_FRONTEND, 'expected' => [ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php new file mode 100644 index 0000000000000..aaa3aa6c97a7e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php @@ -0,0 +1,85 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Code\Generator; + +use Magento\Framework\Code\Generator; +use Magento\Framework\Logger\Monolog as MagentoMonologLogger; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Psr\Log\LoggerInterface; + +class AutoloaderTest extends TestCase +{ + /** + * This method exists to fix the wrong return type hint on \Magento\Framework\App\ObjectManager::getInstance. + * This way the IDE knows it's dealing with an instance of \Magento\TestFramework\ObjectManager and + * not \Magento\Framework\App\ObjectManager. The former has the method addSharedInstance, the latter does not. + * + * @return ObjectManager|\Magento\Framework\App\ObjectManager + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function getTestFrameworkObjectManager() + { + return ObjectManager::getInstance(); + } + + /** + * @before + */ + public function setupLoggerTestDouble() + { + $loggerTestDouble = $this->createMock(LoggerInterface::class); + $this->getTestFrameworkObjectManager()->addSharedInstance($loggerTestDouble, MagentoMonologLogger::class); + } + + /** + * @after + */ + public function removeLoggerTestDouble() + { + $this->getTestFrameworkObjectManager()->removeSharedInstance(MagentoMonologLogger::class); + } + + /** + * @param \RuntimeException $testException + * @return Generator|MockObject + */ + private function createExceptionThrowingGeneratorTestDouble(\RuntimeException $testException) + { + /** @var Generator|MockObject $generatorStub */ + $generatorStub = $this->createMock(Generator::class); + $generatorStub->method('generateClass')->willThrowException($testException); + + return $generatorStub; + } + + public function testLogsExceptionDuringGeneration() + { + $exceptionMessage = 'Test exception thrown during generation'; + $testException = new \RuntimeException($exceptionMessage); + + $loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class); + $loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]); + + $autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException)); + $this->assertNull($autoloader->load(NonExistingClassName::class)); + } + + public function testFiltersDuplicateExceptionMessages() + { + $exceptionMessage = 'Test exception thrown during generation'; + $testException = new \RuntimeException($exceptionMessage); + + $loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class); + $loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]); + + $autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException)); + $autoloader->load(OneNonExistingClassName::class); + $autoloader->load(AnotherNonExistingClassName::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php new file mode 100644 index 0000000000000..e64b3c505acf1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +/** + * \Magento\Framework\Lock\Backend\File test case + */ +class FileLockTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\Lock\Backend\FileLock + */ + private $model; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->model = $this->objectManager->create( + \Magento\Framework\Lock\Backend\FileLock::class, + ['path' => '/tmp'] + ); + } + + public function testLockAndUnlock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + + $this->assertTrue($this->model->lock($name)); + $this->assertTrue($this->model->isLocked($name)); + $this->assertFalse($this->model->lock($name, 2)); + + $this->assertTrue($this->model->unlock($name)); + $this->assertFalse($this->model->isLocked($name)); + } + + public function testUnlockWithoutExistingLock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + $this->assertFalse($this->model->unlock($name)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php new file mode 100644 index 0000000000000..8d0caad5d55e4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; +use Magento\Framework\Lock\LockBackendFactory; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\App\DeploymentConfig\FileReader; +use Magento\Framework\Stdlib\ArrayManager; + +/** + * \Magento\Framework\Lock\Backend\Zookeeper test case + */ +class ZookeeperTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var FileReader + */ + private $configReader; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var LockBackendFactory + */ + private $lockBackendFactory; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @var ZookeeperLock + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + if (!extension_loaded('zookeeper')) { + $this->markTestSkipped('php extension Zookeeper is not installed.'); + } + + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->configReader = $this->objectManager->get(FileReader::class); + $this->lockBackendFactory = $this->objectManager->create(LockBackendFactory::class); + $this->arrayManager = $this->objectManager->create(ArrayManager::class); + $config = $this->configReader->load(ConfigFilePool::APP_ENV); + + if ($this->arrayManager->get('lock/provider', $config) !== 'zookeeper') { + $this->markTestSkipped('Zookeeper is not configured during installation.'); + } + + $this->model = $this->lockBackendFactory->create(); + $this->assertInstanceOf(ZookeeperLock::class, $this->model); + } + + public function testLockAndUnlock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + + $this->assertTrue($this->model->lock($name)); + $this->assertTrue($this->model->isLocked($name)); + $this->assertFalse($this->model->lock($name, 2)); + + $this->assertTrue($this->model->unlock($name)); + $this->assertFalse($this->model->isLocked($name)); + } + + public function testUnlockWithoutExistingLock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + $this->assertFalse($this->model->unlock($name)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Integration/Controller/Adminhtml/IntegrationTest.php b/dev/tests/integration/testsuite/Magento/Integration/Controller/Adminhtml/IntegrationTest.php index 8011873577dc8..4da0c12c6087a 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Controller/Adminhtml/IntegrationTest.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Controller/Adminhtml/IntegrationTest.php @@ -7,6 +7,7 @@ namespace Magento\Integration\Controller\Adminhtml; use Magento\TestFramework\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; /** * \Magento\Integration\Controller\Adminhtml\Integration @@ -20,6 +21,9 @@ class IntegrationTest extends \Magento\TestFramework\TestCase\AbstractBackendCon /** @var \Magento\Integration\Model\Integration */ private $_integration; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); @@ -29,6 +33,9 @@ protected function setUp() $this->_integration = $integration->load('Fixture Integration', 'name'); } + /** + * Test view page. + */ public function testIndexAction() { $this->dispatch('backend/admin/integration/index'); @@ -44,6 +51,9 @@ public function testIndexAction() ); } + /** + * Test creation form. + */ public function testNewAction() { $this->dispatch('backend/admin/integration/new'); @@ -61,6 +71,9 @@ public function testNewAction() ); } + /** + * Test update form. + */ public function testEditAction() { $integrationId = $this->_integration->getId(); @@ -88,12 +101,16 @@ public function testEditAction() ); } + /** + * Test saving. + */ public function testSaveActionUpdateIntegration() { $integrationId = $this->_integration->getId(); $integrationName = $this->_integration->getName(); $this->getRequest()->setParam('id', $integrationId); $url = 'http://magento.ll/endpoint_url'; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'name' => $integrationName, @@ -111,10 +128,14 @@ public function testSaveActionUpdateIntegration() $this->assertRedirect($this->stringContains('backend/admin/integration/index/')); } + /** + * Test saving. + */ public function testSaveActionNewIntegration() { $url = 'http://magento.ll/endpoint_url'; $integrationName = md5(rand()); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'name' => $integrationName, diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php new file mode 100644 index 0000000000000..48d3356525f49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Newsletter\Block\Adminhtml\Subscriber; + +/** + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * + * @see \Magento\Newsletter\Block\Adminhtml\Subscriber\Grid + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var null|\Magento\Framework\ObjectManagerInterface + */ + private $objectManager = null; + /** + * @var null|\Magento\Framework\View\LayoutInterface + */ + private $layout = null; + + /** + * Set up layout. + */ + protected function setUp() + { + parent::setUp(); + + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->layout = $this->objectManager->create(\Magento\Framework\View\LayoutInterface::class); + $this->layout->getUpdate()->load('newsletter_subscriber_grid'); + $this->layout->generateXml(); + $this->layout->generateElements(); + } + + /** + * Check if mass action block exists. + */ + public function testMassActionBlockExists() + { + $this->assertNotFalse( + $this->getMassActionBlock(), + 'Mass action block does not exist in the grid, or it name was changed.' + ); + } + + /** + * Check if mass action id field is correct. + */ + public function testMassActionFieldIdIsCorrect() + { + $this->assertEquals( + 'subscriber_id', + $this->getMassActionBlock()->getMassactionIdField(), + 'Mass action id field is incorrect.' + ); + } + + /** + * Check if function returns correct result. + * + * @magentoDataFixture Magento/Newsletter/_files/subscribers.php + */ + public function testMassActionBlockContainsCorrectIdList() + { + $this->assertEquals( + implode(',', $this->getAllSubscriberIdList()), + $this->getMassActionBlock()->getGridIdsJson(), + 'Function returns incorrect result.' + ); + } + + /** + * Retrieve mass action block. + * + * @return bool|\Magento\Backend\Block\Widget\Grid\Massaction + */ + private function getMassActionBlock() + { + return $this->layout->getBlock('adminhtml.newslettrer.subscriber.grid.massaction'); + } + + /** + * Retrieve list of id of all subscribers. + * + * @return array + */ + private function getAllSubscriberIdList() + { + /** @var \Magento\Framework\App\ResourceConnection $resourceConnection */ + $resourceConnection = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class); + $select = $resourceConnection->getConnection() + ->select() + ->from($resourceConnection->getTableName('newsletter_subscriber')) + ->columns(['subscriber_id' => 'subscriber_id']); + + return $resourceConnection->getConnection()->fetchCol($select); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterQueueTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterQueueTest.php index 5c1a11756c1b1..7a0ac030d120b 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterQueueTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterQueueTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Newsletter\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * @magentoAppArea adminhtml */ @@ -15,6 +17,9 @@ class NewsletterQueueTest extends \Magento\TestFramework\TestCase\AbstractBacken */ protected $_model; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); @@ -23,6 +28,9 @@ protected function setUp() ); } + /** + * @inheritDoc + */ protected function tearDown() { /** @@ -47,6 +55,7 @@ public function testSaveActionQueueTemplateAndVerifySuccessMessage() 'subject' => 'test subject', 'text' => 'newsletter text', ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postForQueue); // Loading by code, since ID will vary. template_code is not actually used to load anywhere else. diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php index 50e89d92e434c..4a5b190d789d7 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php @@ -20,6 +20,9 @@ class NewsletterTemplateTest extends \Magento\TestFramework\TestCase\AbstractBac */ protected $model; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); @@ -39,6 +42,9 @@ protected function setUp() ); } + /** + * @inheritDoc + */ protected function tearDown() { /** diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Controller/Transparent/ResponseTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Transparent/ResponseTest.php new file mode 100644 index 0000000000000..e2bb1d7b8f7c6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Transparent/ResponseTest.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Controller\Transparent; + +use Magento\Checkout\Model\Session; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Session\Generic as GenericSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\PaymentMethodManagementInterface; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Tests PayPal transparent response controller. + */ +class ResponseTest extends AbstractController +{ + /** + * Tests setting credit card expiration month and year to payment from PayPal response. + * + * @param string $currentDateTime + * @param string $paypalExpDate + * @param string $expectedCcMonth + * @param string $expectedCcYear + * @throws NoSuchEntityException + * + * @magentoConfigFixture current_store payment/payflowpro/active 1 + * @magentoDataFixture Magento/Sales/_files/quote.php + * @dataProvider paymentCcExpirationDateDataProvider + */ + public function testPaymentCcExpirationDate( + string $currentDateTime, + string $paypalExpDate, + string $expectedCcMonth, + string $expectedCcYear + ) { + $reservedOrderId = 'test01'; + $postData = [ + 'EXPDATE' => $paypalExpDate, + 'AMT' => '0.00', + 'RESPMSG' => 'Verified', + 'CVV2MATCH' => 'Y', + 'PNREF' => 'A10AAD866C87', + 'SECURETOKEN' => '3HYEHfG06skydAdBXbpIl8QJZ', + 'AVSDATA' => 'YNY', + 'RESULT' => '0', + 'IAVS' => 'N', + 'AVSADDR' => 'Y', + 'SECURETOKENID' => 'yqanLisRZbI0HAG8q3SbbKbhiwjNZAGf', + ]; + + $quote = $this->getQuote($reservedOrderId); + $this->getRequest()->setPostValue($postData); + + /** @var Session $checkoutSession */ + $checkoutSession = $this->_objectManager->get(GenericSession::class); + $checkoutSession->setQuoteId($quote->getId()); + $this->setCurrentDateTime($currentDateTime); + + $this->dispatch('paypal/transparent/response'); + + /** @var PaymentMethodManagementInterface $paymentManagment */ + $paymentManagment = $this->_objectManager->get(PaymentMethodManagementInterface::class); + $payment = $paymentManagment->get($quote->getId()); + + $this->assertEquals($expectedCcMonth, $payment->getCcExpMonth()); + $this->assertEquals($expectedCcYear, $payment->getCcExpYear()); + } + + /** + * @return array + */ + public function paymentCcExpirationDateDataProvider(): array + { + return [ + 'Expiration year in current century' => [ + 'currentDateTime' => '2019-07-05 00:00:00', + 'paypalExpDate' => '0321', + 'expectedCcMonth' => 3, + 'expectedCcYear' => 2021 + ], + 'Expiration year in next century' => [ + 'currentDateTime' => '2099-01-01 00:00:00', + 'paypalExpDate' => '1002', + 'expectedCcMonth' => 10, + 'expectedCcYear' => 2102 + ] + ]; + } + + /** + * Sets current date and time. + * + * @param string $date + */ + private function setCurrentDateTime(string $dateTime) + { + $dateTime = new \DateTime($dateTime, new \DateTimeZone('UTC')); + $dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $dateTimeFactory->method('create') + ->willReturn($dateTime); + + $this->_objectManager->addSharedInstance($dateTimeFactory, DateTimeFactory::class); + } + + /** + * Gets quote by reserved order ID. + * + * @param string $reservedOrderId + * @return CartInterface + */ + private function getQuote(string $reservedOrderId): CartInterface + { + $searchCriteria = $this->_objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index eb80da6d21b19..ee05e402bc689 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -72,7 +72,7 @@ protected function setUp() $this->api = $this->getMockBuilder(Nvp::class) ->disableOriginalConstructor() - ->setMethods(['call', 'getExportedShippingAddress', 'getExportedBillingAddress']) + ->setMethods(['call', 'getExportedShippingAddress', 'getExportedBillingAddress', 'getShippingRateCode']) ->getMock(); $this->api->expects($this->any()) @@ -303,6 +303,7 @@ public function testReturnFromPaypal() public function testReturnFromPaypalButton() { $quote = $this->getFixtureQuote(); + $quote->getShippingAddress()->setShippingMethod(''); $this->prepareCheckoutModel($quote); $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 1); @@ -318,6 +319,8 @@ public function testReturnFromPaypalButton() $this->assertEquals($exportedShippingData['telephone'], $shippingAddress->getTelephone()); $this->assertEquals($exportedShippingData['email'], $shippingAddress->getEmail()); + $this->assertEquals('flatrate_flatrate', $shippingAddress->getShippingMethod()); + $this->assertEquals([$exportedShippingData['street']], $billingAddress->getStreet()); $this->assertEquals($exportedShippingData['firstname'], $billingAddress->getFirstname()); $this->assertEquals($exportedShippingData['city'], $billingAddress->getCity()); @@ -512,6 +515,9 @@ private function prepareCheckoutModel(Quote $quote, $prefix = '') $this->api->method('getExportedShippingAddress') ->will($this->returnValue($exportedShippingAddress)); + $this->api->method('getShippingRateCode') + ->willReturn('flatrate_flatrate Flat Rate - Fixed'); + $this->paypalInfo->method('importToPayment') ->with($this->api, $quote->getPayment()); } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index 3a516befc37ff..ce5180710728f 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php @@ -311,6 +311,26 @@ public function testAssignCustomerWithAddressChange() } } + /** + * Customer has address with country which not allowed in website + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @magentoDataFixture Magento/Backend/_files/allowed_countries_fr.php + * @return void + */ + public function testAssignCustomerWithAddressChangeWithNotAllowedCountry() + { + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $customerData = $this->_prepareQuoteForTestAssignCustomerWithAddressChange($quote); + $quote->assignCustomerWithAddressChange($customerData); + + /** Check that addresses are empty */ + $this->assertNull($quote->getBillingAddress()->getCountryId()); + $this->assertNull($quote->getShippingAddress()->getCountryId()); + } + /** * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php */ diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/ResourceModel/Quote/Item/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/ResourceModel/Quote/Item/CollectionTest.php new file mode 100644 index 0000000000000..7bfdb096673cc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/ResourceModel/Quote/Item/CollectionTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\ResourceModel\Quote\Item; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\ResourceModel\Quote\Item\Collection as QuoteItemCollection; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Tests Magento\Quote\Model\ResourceModel\Quote\Item\Collection. + */ +class CollectionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + } + + /** + * Covers case when during quote item collection load product exists in db but not accessible. + * + * @magentoDataFixture Magento/Sales/_files/quote.php + * @return void + */ + public function testLoadCollectionWithNotAccessibleProduct() + { + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quote->load('test01', 'reserved_order_id'); + + $this->assertCount(1, $quote->getItemsCollection()); + + $product = $this->productRepository->get('simple'); + /** @var ProductCollection $productCollection */ + $productCollection = $this->objectManager->create(ProductCollection::class); + $this->setPropertyValue($productCollection, '_isCollectionLoaded', true); + /** @var ProductCollectionFactory|\PHPUnit_Framework_MockObject_MockObject $productCollectionFactoryMock */ + $productCollectionFactoryMock = $this->createMock(ProductCollectionFactory::class); + $productCollectionFactoryMock->expects($this->any())->method('create')->willReturn($productCollection); + + /** @var QuoteItemCollection $quoteItemCollection */ + $quoteItemCollection = $this->objectManager->create( + QuoteItemCollection::class, + [ + 'productCollectionFactory' => $productCollectionFactoryMock, + ] + ); + + $quoteItemCollection->setQuote($quote); + $this->assertCount(1, $quoteItemCollection); + $item = $quoteItemCollection->getItemByColumnValue('product_id', $product->getId()); + + $this->assertNotNull($item); + $this->assertTrue($item->isDeleted()); + } + + /** + * Set object non-public property value. + * + * @param object $object + * @param string $propertyName + * @param mixed $value + * @return void + */ + private function setPropertyValue($object, string $propertyName, $value) + { + $reflectionClass = new \ReflectionClass($object); + if ($reflectionClass->hasProperty($propertyName)) { + $reflectionProperty = $reflectionClass->getProperty($propertyName); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $value); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php new file mode 100644 index 0000000000000..e0c38775101a3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Rss\Controller\Feed; + +/** + * Test for \Magento\Rss\Controller\Feed\Index + */ +class IndexTest extends \Magento\TestFramework\TestCase\AbstractBackendController +{ + /** + * @var \Magento\Rss\Model\UrlBuilder + */ + private $urlBuilder; + + /** + * @var \Magento\Customer\Api\CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var \Magento\Wishlist\Model\Wishlist + */ + private $wishlist; + + /** + * @var \Magento\Customer\Model\Session + */ + private $customerSession; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->urlBuilder = $this->_objectManager->get(\Magento\Rss\Model\UrlBuilder::class); + $this->customerRepository = $this->_objectManager->get( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $this->wishlist = $this->_objectManager->get(\Magento\Wishlist\Model\Wishlist::class); + $this->customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + } + + /** + * Check Rss response. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + * @magentoConfigFixture current_store rss/wishlist/active 1 + * @magentoConfigFixture current_store rss/config/active 1 + * @return void + */ + public function testRssResponse() + { + $customerEmail = 'customer@example.com'; + $customer = $this->customerRepository->get($customerEmail); + $customerId = $customer->getId(); + $this->customerSession->setCustomerId($customerId); + $wishlistId = $this->wishlist->loadByCustomerId($customerId)->getId(); + $this->dispatch($this->getLink($customerId, $customerEmail, $wishlistId)); + $body = $this->getResponse()->getBody(); + + $this->assertContains('John Smith\'s Wishlist', $body); + } + + /** + * Check Rss with incorrect wishlist id. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + * @magentoConfigFixture current_store rss/wishlist/active 1 + * @magentoConfigFixture current_store rss/config/active 1 + * @return void + */ + public function testRssResponseWithIncorrectWishlistId() + { + $firstCustomerEmail = 'customer@example.com'; + $secondCustomerEmail = 'customer_two@example.com'; + $firstCustomer = $this->customerRepository->get($firstCustomerEmail); + $secondCustomer = $this->customerRepository->get($secondCustomerEmail); + + $firstCustomerId = $firstCustomer->getId(); + $secondCustomerId = $secondCustomer->getId(); + $this->customerSession->setCustomerId($firstCustomerId); + $wishlistId = $this->wishlist->loadByCustomerId($secondCustomerId, true)->getId(); + $this->dispatch($this->getLink($firstCustomerId, $firstCustomerEmail, $wishlistId)); + $body = $this->getResponse()->getBody(); + + $this->assertContains('<title>404 Not Found', $body); + } + + /** + * @param mixed $customerId + * @param string $customerEmail + * @param mixed $wishlistId + * @return string + */ + private function getLink($customerId, string $customerEmail, $wishlistId): string + { + return 'rss/feed/index/type/wishlist/data/' + . base64_encode($customerId . ',' . $customerEmail) + . '/wishlist_id/' . $wishlistId; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/TotalsTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/TotalsTest.php new file mode 100644 index 0000000000000..1125fc1730718 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/TotalsTest.php @@ -0,0 +1,59 @@ +layout = $this->_objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(Totals::class, 'totals_block'); + $this->orderFactory = $this->_objectManager->get(OrderFactory::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_free_shipping_by_coupon.php + */ + public function testShowShippingCoupon() + { + /** @var Order $order */ + $order = $this->orderFactory->create(); + $order->loadByIncrementId('100000001'); + + $this->block->setOrder($order); + $this->block->toHtml(); + + $shippingTotal = $this->block->getTotal('shipping'); + $this->assertNotFalse($shippingTotal, 'Shipping method is absent on the total\'s block.'); + $this->assertContains( + '1234567890', + $shippingTotal->getLabel(), + 'Coupon code is absent in the shipping method label name.' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php index da0f2be856c51..26b5f58760da8 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php @@ -78,6 +78,7 @@ public function testExecuteWithPaymentOperation() 'email' => $email, ] ]; + $this->getRequest()->setMethod('POST'); $this->getRequest()->setPostValue(['order' => $data]); /** @var OrderService|MockObject $orderService */ diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php index 7110f39ee532c..f8e468ac42a32 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php @@ -43,6 +43,7 @@ protected function setUp() public function testLoadBlockAction() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('block', ','); $this->getRequest()->setParam('json', 1); $this->dispatch('backend/sales/order_create/loadBlock'); @@ -60,6 +61,7 @@ public function testLoadBlockActionData() )->addProducts( [$product->getId() => ['qty' => 1]] ); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('block', 'data'); $this->getRequest()->setParam('json', 1); $this->dispatch('backend/sales/order_create/loadBlock'); @@ -135,6 +137,7 @@ public function testLoadBlockShippingMethod() */ public function testLoadBlockActions($block, $expected) { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('block', $block); $this->getRequest()->setParam('json', 1); $this->dispatch('backend/sales/order_create/loadBlock'); @@ -142,6 +145,9 @@ public function testLoadBlockActions($block, $expected) $this->assertContains($expected, $html); } + /** + * @return array + */ public function loadBlockActionsDataProvider() { return [ @@ -166,6 +172,7 @@ public function testLoadBlockActionItems() )->addProducts( [$product->getId() => ['qty' => 1]] ); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('block', 'items'); $this->getRequest()->setParam('json', 1); $this->dispatch('backend/sales/order_create/loadBlock'); @@ -308,6 +315,7 @@ public function testDeniedSaveAction() \Magento\TestFramework\Helper\Bootstrap::getInstance() ->loadArea('adminhtml'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/sales/order_create/save'); $this->assertEquals('403', $this->getResponse()->getHttpResponseCode()); } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon.php new file mode 100644 index 0000000000000..57ccffadaa4d0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon.php @@ -0,0 +1,35 @@ +create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setFreeShipping('1'); + +/** @var Order $order */ +$order->setShippingDescription('Flat Rate - Fixed') + ->setShippingAmount(0) + ->setCouponCode('1234567890') + ->setDiscountDescription('1234567890') + ->addItem($orderItem); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon_rollback.php new file mode 100644 index 0000000000000..1fb4b4636ab29 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon_rollback.php @@ -0,0 +1,8 @@ +get(ScopeConfigInterface::class); + $this->defaultTimezone = $scopeConfig->getValue(AdminBackendConfig::XML_PATH_GENERAL_LOCALE_TIMEZONE); + + $this->collection = Bootstrap::getObjectManager()->create(Collection::class); + } + /** * @magentoDataFixture Magento/SalesRule/_files/rules.php * @magentoDataFixture Magento/SalesRule/_files/coupons.php @@ -21,12 +48,8 @@ class CollectionTest extends \PHPUnit\Framework\TestCase */ public function testSetValidationFilter($couponCode, $expectedItems) { - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $items = array_values($collection->setValidationFilter(1, 0, $couponCode)->getItems()); - - $ids = []; + /** @var \Magento\SalesRule\Model\Rule[] $items */ + $items = array_values($this->collection->setValidationFilter(1, 0, $couponCode)->getItems()); $this->assertEquals( count($expectedItems), @@ -34,6 +57,7 @@ public function testSetValidationFilter($couponCode, $expectedItems) 'Invalid number of items in the result collection' ); + $ids = []; foreach ($items as $key => $item) { $this->assertEquals($expectedItems[$key], $item->getName()); $this->assertFalse( @@ -71,7 +95,7 @@ public function setValidationFilterDataProvider() */ public function testSetValidationFilterWithGroup() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\SalesRule\Model\Rule $rule */ $rule = $objectManager->get(\Magento\Framework\Registry::class) @@ -82,13 +106,8 @@ public function testSetValidationFilterWithGroup() $quote->load('test_order_item_with_items', 'reserved_order_id'); //gather only the existing rules that obey the validation filter - /** @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection $ruleCollection */ - $ruleCollection = $objectManager->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $appliedRulesArray = array_keys( - $ruleCollection->setValidationFilter( + $this->collection->setValidationFilter( $quote->getStore()->getWebsiteId(), 0, '', @@ -108,7 +127,7 @@ public function testSetValidationFilterWithGroup() */ public function testSetValidationFilterAnyCategory() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\SalesRule\Model\Rule $rule */ $rule = $objectManager->get(\Magento\Framework\Registry::class) @@ -119,13 +138,8 @@ public function testSetValidationFilterAnyCategory() $quote->load('test_order_item_with_items', 'reserved_order_id'); //gather only the existing rules that obey the validation filter - /** @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection $ruleCollection */ - $ruleCollection = $objectManager->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $appliedRulesArray = array_keys( - $ruleCollection->setValidationFilter( + $this->collection->setValidationFilter( $quote->getStore()->getWebsiteId(), 0, '', @@ -146,20 +160,15 @@ public function testSetValidationFilterAnyCategory() */ public function testSetValidationFilterOther() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\Quote\Model\Quote $quote */ $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); //gather only the existing rules that obey the validation filter - /** @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection $ruleCollection */ - $ruleCollection = $objectManager->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $appliedRulesArray = array_keys( - $ruleCollection->setValidationFilter( + $this->collection->setValidationFilter( $quote->getStore()->getWebsiteId(), 0, '', @@ -181,11 +190,8 @@ public function testSetValidationFilterOther() public function testMultiRulesWithTimezone() { $this->setSpecificTimezone('Europe/Kiev'); - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $collection->addWebsiteGroupDateFilter(1, 0); - $items = array_values($collection->getItems()); + $this->collection->addWebsiteGroupDateFilter(1, 0); + $items = array_values($this->collection->getItems()); $this->assertNotEmpty($items); } @@ -200,11 +206,8 @@ public function testMultiRulesWithTimezone() public function testMultiRulesWithDifferentTimezone() { $this->setSpecificTimezone('Australia/Sydney'); - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $collection->addWebsiteGroupDateFilter(1, 0); - $items = array_values($collection->getItems()); + $this->collection->addWebsiteGroupDateFilter(1, 0); + $items = array_values($this->collection->getItems()); $this->assertNotEmpty($items); } @@ -224,7 +227,7 @@ protected function setSpecificTimezone($timezone) ] ] ]; - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config\Factory::class) + Bootstrap::getObjectManager()->get(\Magento\Config\Model\Config\Factory::class) ->create() ->addData($localeData) ->save(); @@ -239,11 +242,9 @@ protected function setSpecificTimezone($timezone) */ public function testAddAttributeInConditionFilterPositive() { - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $collection->addAttributeInConditionFilter('attribute_for_sales_rule_1'); - $item = $collection->getFirstItem(); + $this->collection->addAttributeInConditionFilter('attribute_for_sales_rule_1'); + /** @var \Magento\SalesRule\Model\Rule $item */ + $item = $this->collection->getFirstItem(); $this->assertEquals('50% Off on some attribute', $item->getName()); } @@ -256,16 +257,57 @@ public function testAddAttributeInConditionFilterPositive() */ public function testAddAttributeInConditionFilterNegative() { - $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class - ); - $collection->addAttributeInConditionFilter('attribute_for_sales_rule_2'); - $this->assertEquals(0, $collection->count()); + $this->collection->addAttributeInConditionFilter('attribute_for_sales_rule_2'); + $this->assertEquals(0, $this->collection->count()); + } + + /** + * @magentoAppIsolation disabled + * @magentoDataFixture Magento/SalesRule/_files/multi_websites_rules.php + * @dataProvider addWebsiteFilterDataProvider + * @param string[] $websiteCodes + * @param int $count + */ + public function testAddWebsiteFilter(array $websiteCodes, int $count) + { + $websiteRepository = Bootstrap::getObjectManager()->get(WebsiteRepositoryInterface::class); + $websiteIds = []; + foreach ($websiteCodes as $websiteCode) { + $websiteIds[] = (int) $websiteRepository->get($websiteCode)->getId(); + } + + $this->collection->addWebsiteFilter($websiteIds); + $this->assertEquals($count, $this->collection->getSize()); + $this->assertCount($count, $this->collection->getItems()); } - public function tearDown() + /** + * @return array + */ + public function addWebsiteFilterDataProvider(): array + { + return [ + [ + ['base'], + 4, + ], + [ + ['test'], + 2, + ], + [ + ['base', 'test'], + 5, + ], + ]; + } + + /** + * @inheritDoc + */ + protected function tearDown() { // restore default timezone - $this->setSpecificTimezone('America/Los_Angeles'); + $this->setSpecificTimezone($this->defaultTimezone); } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules.php new file mode 100644 index 0000000000000..a43df3d67c077 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules.php @@ -0,0 +1,16 @@ +setWebsiteIds($website->getId()) + ->save(); + +/** @var \Magento\SalesRule\Model\Rule $rule3 */ +$rule3->setWebsiteIds(implode(',', [1, $website->getId()])) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules_rollback.php new file mode 100644 index 0000000000000..9e0e01b9fc51c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/multi_websites_rules_rollback.php @@ -0,0 +1,8 @@ +create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule1 */ +$rule1 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule1->setName( '#1' )->setIsActive( 1 @@ -27,9 +27,9 @@ )->setSortOrder(1) ->save(); -/** @var \Magento\SalesRule\Model\Rule $rule */ -$rule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule2 */ +$rule2 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule2->setName( '#2' )->setIsActive( 1 @@ -50,9 +50,9 @@ )->setSortOrder(2) ->save(); -/** @var \Magento\SalesRule\Model\Rule $rule */ -$rule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule3 */ +$rule3 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule3->setName( '#3' )->setIsActive( 1 @@ -73,9 +73,9 @@ )->setSortOrder(3) ->save(); -/** @var \Magento\SalesRule\Model\Rule $rule */ -$rule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule4 */ +$rule4 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule4->setName( '#4' )->setIsActive( 1 @@ -96,9 +96,9 @@ )->setSortOrder(4) ->save(); -/** @var \Magento\SalesRule\Model\Rule $rule */ -$rule = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); -$rule->setName( +/** @var \Magento\SalesRule\Model\Rule $rule5 */ +$rule5 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Rule::class); +$rule5->setName( '#5' )->setIsActive( 1 diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php new file mode 100644 index 0000000000000..d464c51050834 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php @@ -0,0 +1,168 @@ +accountManagement = $this->_objectManager->create(AccountManagementInterface::class); + $this->formKey = $this->_objectManager->create(FormKey::class); + $logger = $this->createMock(LoggerInterface::class); + $this->session = $this->_objectManager->create( + Session::class, + [$logger] + ); + $this->captchaHelper = $this->_objectManager->create(CaptchaHelper::class); + $customer = $this->accountManagement->authenticate('customer@example.com', 'password'); + $this->session->setCustomerDataAsLoggedIn($customer); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testExecute() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store customer/captcha/forms product_sendtofriend_form + */ + public function testWithCaptchaFailed() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store customer/captcha/forms product_sendtofriend_form + * + */ + public function testWithCaptchaSuccess() + { + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha('product_sendtofriend_form'); + $captchaModel->generate(); + $word = $captchaModel->getWord(); + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'captcha' => [ + 'product_sendtofriend_form' => $word + ], + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php index bd6c900cf203c..4f9778febfb1c 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php @@ -9,10 +9,19 @@ namespace Magento\Tax\Model\Sales\Total\Quote; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Quote\Model\Quote; use Magento\Tax\Model\Config; use Magento\Tax\Model\Calculation; +use Magento\Quote\Model\Quote\Item\Updater; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Api\SearchCriteriaInterface; /** + * Setup utility for quote + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SetupUtil @@ -593,7 +602,7 @@ protected function createCartRule($ruleDataOverride) * * @param array $quoteData * @param \Magento\Customer\Api\Data\CustomerInterface $customer - * @return \Magento\Quote\Model\Quote + * @return Quote */ protected function createQuote($quoteData, $customer) { @@ -618,8 +627,8 @@ protected function createQuote($quoteData, $customer) $quoteBillingAddress = $this->objectManager->create(\Magento\Quote\Model\Quote\Address::class); $quoteBillingAddress->importCustomerAddressData($addressService->getById($billingAddress->getId())); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->setStoreId(1) ->setIsActive(true) ->setIsMultiShipping(false) @@ -633,7 +642,7 @@ protected function createQuote($quoteData, $customer) /** * Add products to quote * - * @param \Magento\Quote\Model\Quote $quote + * @param Quote $quote * @param array $itemsData * @return $this */ @@ -656,7 +665,8 @@ protected function addProductToQuote($quote, $itemsData) * Create a quote based on given data * * @param array $quoteData - * @return \Magento\Quote\Model\Quote + * + * @return Quote */ public function setupQuote($quoteData) { @@ -665,7 +675,9 @@ public function setupQuote($quoteData) $quote = $this->createQuote($quoteData, $customer); $this->addProductToQuote($quote, $quoteData['items']); - + if (isset($quoteData['update_items'])) { + $this->updateItems($quote, $quoteData['update_items']); + } //Set shipping amount if (isset($quoteData['shipping_method'])) { $quote->getShippingAddress()->setShippingMethod($quoteData['shipping_method']); @@ -682,4 +694,33 @@ public function setupQuote($quoteData) return $quote; } + + /** + * Update quote items + * + * @param Quote $quote + * @param array $items + * + * @return void + */ + private function updateItems(Quote $quote, array $items) + { + $updater = $this->objectManager->get(Updater::class); + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $filter = $this->objectManager->create(Filter::class); + $filter->setField('sku')->setValue(array_keys($items)); + $filterGroup = $this->objectManager->create(FilterGroup::class); + $filterGroup->setFilters([$filter]); + $searchCriteria = $this->objectManager->create(SearchCriteriaInterface::class); + $searchCriteria->setFilterGroups([$filterGroup]); + $products = $productRepository->getList($searchCriteria)->getItems(); + /** @var ProductInterface $product */ + foreach ($products as $product) { + $quoteItem = $quote->getItemByProduct($product); + $updater->update( + $quoteItem, + $items[$product->getSku()] + ); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php index 0513dd1c7d3c4..ebf2c2eea9553 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php @@ -11,6 +11,7 @@ require_once __DIR__ . '/SetupUtil.php'; require_once __DIR__ . '/../../../../_files/tax_calculation_data_aggregated.php'; +require_once __DIR__ . '/../../../../_files/full_discount_with_tax.php'; /** * Class TaxTest @@ -124,6 +125,40 @@ public function testCollect() ); } + /** + * Test taxes collection with full discount for quote. + * + * Test tax calculation and price when the discount may be bigger than total + * This method will test the collector through $quote->collectTotals() method + * + * @see \Magento\SalesRule\Model\Utility::deltaRoundingFix + * @magentoDataFixture Magento/Tax/_files/full_discount_with_tax.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testFullDiscountWithDeltaRoundingFix() + { + global $fullDiscountIncTax; + $configData = $fullDiscountIncTax['config_data']; + $quoteData = $fullDiscountIncTax['quote_data']; + $expectedResults = $fullDiscountIncTax['expected_result']; + + /** @var \Magento\Framework\ObjectManagerInterface $objectManager */ + $objectManager = Bootstrap::getObjectManager(); + + //Setup tax configurations + $this->setupUtil = new SetupUtil($objectManager); + $this->setupUtil->setupTax($configData); + + $quote = $this->setupUtil->setupQuote($quoteData); + + $quote->collectTotals(); + + $quoteAddress = $quote->getShippingAddress(); + + $this->verifyResult($quoteAddress, $expectedResults); + } + /** * Verify fields in quote item * diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/full_discount_with_tax.php b/dev/tests/integration/testsuite/Magento/Tax/_files/full_discount_with_tax.php new file mode 100644 index 0000000000000..2b5ef07de341a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/full_discount_with_tax.php @@ -0,0 +1,120 @@ + [ + 'config_overrides' => [ + Config::CONFIG_XML_PATH_APPLY_AFTER_DISCOUNT => 0, + Config::CONFIG_XML_PATH_DISCOUNT_TAX => 1, + Config::XML_PATH_ALGORITHM => 'ROW_BASE_CALCULATION', + Config::CONFIG_XML_PATH_SHIPPING_TAX_CLASS => SetupUtil::SHIPPING_TAX_CLASS, + ], + 'tax_rate_overrides' => [ + SetupUtil::TAX_RATE_TX => 18, + SetupUtil::TAX_RATE_SHIPPING => 0, + ], + 'tax_rule_overrides' => [ + [ + 'code' => 'Product Tax Rule', + 'product_tax_class_ids' => [ + SetupUtil::PRODUCT_TAX_CLASS_1 + ], + ], + [ + 'code' => 'Shipping Tax Rule', + 'product_tax_class_ids' => [ + SetupUtil::SHIPPING_TAX_CLASS + ], + 'tax_rate_ids' => [ + SetupUtil::TAX_RATE_SHIPPING, + ], + ], + ], + ], + 'quote_data' => [ + 'billing_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'shipping_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'items' => [ + [ + 'sku' => 'simple1', + 'price' => 2542.37, + 'qty' => 2, + ] + ], + 'shipping_method' => 'free', + 'shopping_cart_rules' => [ + [ + 'discount_amount' => 100 + ], + ], + ], + 'expected_result' => [ + 'address_data' => [ + 'subtotal' => 5084.74, + 'base_subtotal' => 5084.74, + 'subtotal_incl_tax' => 5999.99, + 'base_subtotal_incl_tax' => 5999.99, + 'tax_amount' => 915.25, + 'base_tax_amount' => 915.25, + 'shipping_amount' => 0, + 'base_shipping_amount' => 0, + 'shipping_incl_tax' => 0, + 'base_shipping_incl_tax' => 0, + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'discount_amount' => -5999.99, + 'base_discount_amount' => -5999.99, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + 'grand_total' => 0, + 'base_grand_total' => 0, + 'applied_taxes' => [ + SetupUtil::TAX_RATE_TX => [ + 'percent' => 18, + 'amount' => 915.25, + 'base_amount' => 915.25, + 'rates' => [ + [ + 'code' => SetupUtil::TAX_RATE_TX, + 'title' => SetupUtil::TAX_RATE_TX, + 'percent' => 18, + ], + ], + ] + ], + ], + 'items_data' => [ + 'simple1' => [ + 'row_total' => 5084.74, + 'base_row_total' => 5084.74, + 'tax_percent' => 18, + 'price' => 2542.37, + 'base_price' => 2542.37, + 'price_incl_tax' => 3000, + 'base_price_incl_tax' => 3000, + 'row_total_incl_tax' => 5999.99, + 'base_row_total_incl_tax' => 5999.99, + 'tax_amount' => 915.25, + 'base_tax_amount' => 915.25, + 'discount_amount' => 5999.99, + 'base_discount_amount' => 5999.99, + 'discount_percent' => 100, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + ], + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/excluding_tax_apply_origin_price_with_custom_price.php b/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/excluding_tax_apply_origin_price_with_custom_price.php new file mode 100644 index 0000000000000..081b8e0a24620 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/excluding_tax_apply_origin_price_with_custom_price.php @@ -0,0 +1,92 @@ + [ + SetupUtil::CONFIG_OVERRIDES => [ + Config::CONFIG_XML_PATH_APPLY_ON => 1, + ], + SetupUtil::TAX_RATE_OVERRIDES => [ + SetupUtil::TAX_RATE_TX => 8.25, + SetupUtil::TAX_STORE_RATE => 8.25, + ], + SetupUtil::TAX_RULE_OVERRIDES => [ + ], + ], + 'quote_data' => [ + 'billing_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'shipping_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'items' => [ + [ + 'sku' => 'simple1', + 'price' => 16.24, + 'qty' => 1, + ], + ], + 'update_items' => [ + 'simple1' => [ + 'custom_price' => 14, + 'qty' => 1, + ], + ], + ], + 'expected_results' => [ + 'address_data' => [ + 'subtotal' => 14, + 'base_subtotal' => 14, + 'subtotal_incl_tax' => 15.34, + 'base_subtotal_incl_tax' => 15.34, + 'tax_amount' => 1.34, + 'base_tax_amount' => 1.34, + 'shipping_amount' => 0, + 'base_shipping_amount' => 0, + 'shipping_incl_tax' => 0, + 'base_shipping_incl_tax' => 0, + 'shipping_taxable' => 0, + 'base_shipping_taxable' => 0, + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'discount_amount' => 0, + 'base_discount_amount' => 0, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + 'grand_total' => 15.34, + 'base_grand_total' => 15.34, + ], + 'items_data' => [ + 'simple1' => [ + 'row_total' => 14, + 'base_row_total' => 14, + 'tax_percent' => 8.25, + 'price' => 14, + 'custom_price' => 14, + 'original_custom_price' => 14, + 'base_price' => 14, + 'price_incl_tax' => 15.34, + 'base_price_incl_tax' => 15.34, + 'row_total_incl_tax' => 15.34, + 'base_row_total_incl_tax' => 15.34, + 'tax_amount' => 1.34, + 'base_tax_amount' => 1.34, + 'discount_amount' => 0, + 'base_discount_amount' => 0, + 'discount_percent' => 0, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + ], + ], + ], +]; diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/including_tax_with_custom_price.php b/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/including_tax_with_custom_price.php new file mode 100644 index 0000000000000..290c133f455f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/including_tax_with_custom_price.php @@ -0,0 +1,93 @@ + [ + SetupUtil::CONFIG_OVERRIDES => [ + Config::CONFIG_XML_PATH_PRICE_INCLUDES_TAX => 1, + Config::CONFIG_XML_PATH_APPLY_ON => 0, + ], + SetupUtil::TAX_RATE_OVERRIDES => [ + SetupUtil::TAX_RATE_TX => 8.25, + SetupUtil::TAX_STORE_RATE => 8.25, + ], + SetupUtil::TAX_RULE_OVERRIDES => [ + ], + ], + 'quote_data' => [ + 'billing_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'shipping_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'items' => [ + [ + 'sku' => 'simple1', + 'price' => 16.24, + 'qty' => 1, + ], + ], + 'update_items' => [ + 'simple1' => [ + 'custom_price' => 14, + 'qty' => 1, + ], + ], + ], + 'expected_results' => [ + 'address_data' => [ + 'subtotal' => 12.93, + 'base_subtotal' => 12.93, + 'subtotal_incl_tax' => 14, + 'base_subtotal_incl_tax' => 14, + 'tax_amount' => 1.07, + 'base_tax_amount' => 1.07, + 'shipping_amount' => 0, + 'base_shipping_amount' => 0, + 'shipping_incl_tax' => 0, + 'base_shipping_incl_tax' => 0, + 'shipping_taxable' => 0, + 'base_shipping_taxable' => 0, + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'discount_amount' => 0, + 'base_discount_amount' => 0, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + 'grand_total' => 14, + 'base_grand_total' => 14, + ], + 'items_data' => [ + 'simple1' => [ + 'row_total' => 12.93, + 'base_row_total' => 12.93, + 'tax_percent' => 8.25, + 'price' => 12.93, + 'custom_price' => 12.93, + 'original_custom_price' => 14, + 'base_price' => 12.93, + 'price_incl_tax' => 14, + 'base_price_incl_tax' => 14, + 'row_total_incl_tax' => 14, + 'base_row_total_incl_tax' => 14, + 'tax_amount' => 1.07, + 'base_tax_amount' => 1.07, + 'discount_amount' => 0, + 'base_discount_amount' => 0, + 'discount_percent' => 0, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + ], + ], + ], +]; diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php index f22b48a259685..d3716ae260e0f 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php @@ -3,14 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); /** - * Global array that holds test scenarios data + * Global array that holds test scenarios data. * * @var array */ $taxCalculationData = []; - +//phpcs:disable Magento2.Security.IncludeFile require_once __DIR__ . '/scenarios/excluding_tax_apply_tax_after_discount.php'; require_once __DIR__ . '/scenarios/excluding_tax_apply_tax_after_discount_discount_tax.php'; require_once __DIR__ . '/scenarios/excluding_tax_apply_tax_before_discount.php'; @@ -31,3 +32,5 @@ require_once __DIR__ . '/scenarios/multi_tax_rule_two_row_calculate_subtotal_yes_row.php'; require_once __DIR__ . '/scenarios/multi_tax_rule_two_row_calculate_subtotal_yes_total.php'; require_once __DIR__ . '/scenarios/including_tax_apply_tax_after_discount.php'; +require_once __DIR__ . '/scenarios/including_tax_with_custom_price.php'; +require_once __DIR__ . '/scenarios/excluding_tax_apply_origin_price_with_custom_price.php'; diff --git a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php index 1ea2b28986d8a..1ceb15a63508c 100644 --- a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php @@ -8,6 +8,7 @@ use Magento\Framework\Data\Form\FormKey; use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Framework\App\Request\Http; /** * Class SaveTest @covers \Magento\Theme\Controller\Adminhtml\Design\Config\Save @@ -24,6 +25,11 @@ class SaveTest extends AbstractBackendController */ protected $uri = 'backend/theme/design_config/save'; + /** + * @var string + */ + protected $httpMethod = Http::METHOD_POST; + /** * Test design configuration save valid values. * diff --git a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php index edea65ee810ba..296eb60b9dac3 100644 --- a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php @@ -57,6 +57,7 @@ public function testGetShipConfirmUrlLive() * @magentoConfigFixture current_store carriers/ups/active 1 * @magentoConfigFixture current_store carriers/ups/allowed_methods 1DA,GND * @magentoConfigFixture current_store carriers/ups/free_method GND + * @magentoConfigFixture current_store carriers/ups/type UPS */ public function testCollectFreeRates() { @@ -76,4 +77,29 @@ public function testCollectFreeRates() $this->assertEquals(0, $methods['GND']['price']); $this->assertNotEquals(0, $methods['1DA']['price']); } + + /** + * Check default UPS carrier parameters. + * + * @return void + */ + public function testValidDefaultParameters() + { + $protocolType = $this->carrier->getConfigData('type'); + $this->assertEquals("UPS_XML", $protocolType, "Default type should be UPS_XML"); + + $gatewayUrl = $this->carrier->getConfigData('gateway_url'); + $this->assertEquals( + "https://www.ups.com/using/services/rave/qcostcgi.cgi", + $gatewayUrl, + "Incorrect gateway url" + ); + + $gatewayXmlUrl = $this->carrier->getConfigData('gateway_xml_url'); + $this->assertEquals( + "https://onlinetools.ups.com/ups.app/xml/Rate", + $gatewayXmlUrl, + "Incorrect gateway XML url" + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php new file mode 100644 index 0000000000000..b6055f14e79d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php @@ -0,0 +1,71 @@ +urlFinder = Bootstrap::getObjectManager()->create(UrlFinderInterface::class); + } + + /** + * @dataProvider findOneDataProvider + * @param string $requestPath + * @param string $targetPath + * @param int $redirectType + */ + public function testFindOneByData(string $requestPath, string $targetPath, int $redirectType) + { + $data = [ + UrlRewrite::REQUEST_PATH => $requestPath, + ]; + $urlRewrite = $this->urlFinder->findOneByData($data); + $this->assertEquals($targetPath, $urlRewrite->getTargetPath()); + $this->assertEquals($redirectType, $urlRewrite->getRedirectType()); + } + + /** + * @return array + */ + public function findOneDataProvider(): array + { + return [ + ['string', 'test_page1', 0], + ['string/', 'string', 301], + ['string_permanent', 'test_page1', 301], + ['string_permanent/', 'test_page1', 301], + ['string_temporary', 'test_page1', 302], + ['string_temporary/', 'test_page1', 302], + ['строка', 'test_page1', 0], + ['строка/', 'строка', 301], + [urlencode('строка'), 'test_page2', 0], + [urlencode('строка') . '/', urlencode('строка'), 301], + ['другая_строка', 'test_page1', 302], + ['другая_строка/', 'test_page1', 302], + [urlencode('другая_строка'), 'test_page1', 302], + [urlencode('другая_строка') . '/', 'test_page1', 302], + ['السلسلة', 'test_page1', 0], + [urlencode('السلسلة'), 'test_page1', 0], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php new file mode 100644 index 0000000000000..9edc6507308ee --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php @@ -0,0 +1,42 @@ +create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewrite::class); +foreach ($rewritesData as $rewriteData) { + list ($requestPath, $targetPath, $redirectType) = $rewriteData; + $rewrite = $objectManager->create(\Magento\UrlRewrite\Model\UrlRewrite::class); + $rewrite->setEntityType('custom') + ->setRequestPath($requestPath) + ->setTargetPath($targetPath) + ->setRedirectType($redirectType); + $rewriteResource->save($rewrite); +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php new file mode 100644 index 0000000000000..a98f947d614e0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php @@ -0,0 +1,20 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$urlRewriteCollection = $objectManager->create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection::class); +$collection = $urlRewriteCollection + ->addFieldToFilter('target_path', ['test_page1', 'test_page2']) + ->load() + ->walk('delete'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserTest.php index cfed67b9ddbdb..e62b45862025f 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserTest.php @@ -5,6 +5,7 @@ */ namespace Magento\User\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\TestFramework\Bootstrap; /** @@ -34,6 +35,7 @@ public function testIndexAction() */ public function testSaveActionNoData() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/admin/user/save'); $this->assertRedirect($this->stringContains('backend/admin/user/index/')); } @@ -54,6 +56,7 @@ public function testSaveActionWrongId() $userId = $user->getId(); $this->assertNotEmpty($userId, 'Broken fixture'); $user->delete(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue('user_id', $userId); $this->dispatch('backend/admin/user/save'); $this->assertSessionMessages( @@ -71,6 +74,7 @@ public function testSaveActionWrongId() public function testSaveActionMissingCurrentAdminPassword() { $fixture = uniqid(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'username' => $fixture, @@ -94,6 +98,7 @@ public function testSaveActionMissingCurrentAdminPassword() public function testSaveAction() { $fixture = uniqid(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'username' => $fixture, @@ -121,6 +126,7 @@ public function testSaveAction() */ public function testSaveActionDuplicateUser() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'username' => 'adminUser', @@ -149,6 +155,7 @@ public function testSaveActionDuplicateUser() */ public function testSaveActionPasswordChange($postData, $isPasswordCorrect) { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/admin/user/save'); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/ShareTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/ShareTest.php new file mode 100644 index 0000000000000..47705262caaf3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/ShareTest.php @@ -0,0 +1,92 @@ +login(1); + $this->prepareRequestData(); + $this->dispatch('wishlist/index/send/'); + + $this->assertSessionMessages( + $this->equalTo(['Your wish list has been shared.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Test share wishlist with incorrect data + * + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + */ + public function testShareWishlistWithoutEmails() + { + $this->login(1); + $this->prepareRequestData(true); + $this->dispatch('wishlist/index/send/'); + + $this->assertSessionMessages( + $this->equalTo(['Please enter an email address.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Login the user + * + * @param string $customerId Customer to mark as logged in for the session + * @return void + */ + protected function login($customerId) + { + /** @var Session $session */ + $session = $this->_objectManager->get(Session::class); + $session->loginById($customerId); + } + + /** + * Prepares the request with data + * + * @param bool $invalidData + * @return void + */ + private function prepareRequestData($invalidData = false) + { + Bootstrap::getInstance()->loadArea(Area::AREA_FRONTEND); + $emails = !$invalidData ? 'email-1@example.com,email-2@example.com' : ''; + + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'emails' => $emails, + 'message' => '', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($post); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php new file mode 100644 index 0000000000000..d12dc4a61f8f1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php @@ -0,0 +1,25 @@ +create(\Magento\Customer\Api\CustomerRepositoryInterface::class); +$firstCustomer = $customerRepository->get('customer@example.com'); + +$wishlistForFirstCustomer = $objectManager->create(\Magento\Wishlist\Model\Wishlist::class); +$wishlistForFirstCustomer->loadByCustomerId($firstCustomer->getId(), true); +$item = $wishlistForFirstCustomer->addNewItem($product, new \Magento\Framework\DataObject([])); +$wishlistForFirstCustomer->save(); + +$secondCustomer = $customerRepository->get('customer_two@example.com'); +$wishlistForSecondCustomer = $objectManager->create(\Magento\Wishlist\Model\Wishlist::class); +$wishlistForSecondCustomer->loadByCustomerId($secondCustomer->getId(), true); +$item = $wishlistForSecondCustomer->addNewItem($product, new \Magento\Framework\DataObject([])); +$wishlistForSecondCustomer->save(); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers_rollback.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers_rollback.php new file mode 100644 index 0000000000000..31cab3f9cbc74 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers_rollback.php @@ -0,0 +1,39 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Wishlist\Model\Wishlist $wishlist */ +$wishlist = $objectManager->create(\Magento\Wishlist\Model\Wishlist::class); + +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +try { + $firstCustomer = $customerRepository->get('customer@example.com'); + $wishlist->loadByCustomerId($firstCustomer->getId()); + $wishlist->delete(); + $secondCustomer = $customerRepository->get('customer_two@example.com'); + $wishlist->loadByCustomerId($secondCustomer->getId()); + $wishlist->delete(); +} catch (NoSuchEntityException $e) { + /** Tests which are wrapped with MySQL transaction clear all data by transaction rollback. */ +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../../Magento/Customer/_files/two_customers_rollback.php'; +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_rollback.php'; diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/cc-form.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/cc-form.test.js index df6996afeb965..6062439db2365 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/cc-form.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/cc-form.test.js @@ -15,6 +15,18 @@ define([ describe('Magento_Braintree/js/view/payment/method-renderer/cc-form', function () { var injector = new Squire(), mocks = { + 'Magento_Checkout/js/model/checkout-data-resolver': { + + /** Stub */ + applyBillingAddress: function () { + return true; + }, + + /** Stub */ + resolveBillingAddress: function () { + return true; + } + }, 'Magento_Checkout/js/model/quote': { billingAddress: ko.observable(), shippingAddress: ko.observable(), diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/paypal.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/paypal.test.js index a2373cfb99091..d58c301e5d934 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/paypal.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Braintree/frontend/js/view/payment/method-renderer/paypal.test.js @@ -14,6 +14,18 @@ define([ var injector = new Squire(), mocks = { + 'Magento_Checkout/js/model/checkout-data-resolver': { + + /** Stub */ + applyBillingAddress: function () { + return true; + }, + + /** Stub */ + resolveBillingAddress: function () { + return true; + } + }, 'Magento_Checkout/js/model/quote': { billingAddress: ko.observable(), shippingAddress: ko.observable({ diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js index 12e12eb492c89..cc40386a7779d 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js @@ -24,6 +24,18 @@ define([ return true; }).and.callThrough(), mocks = { + 'Magento_Checkout/js/model/checkout-data-resolver': { + + /** Stub */ + applyBillingAddress: function () { + return true; + }, + + /** Stub */ + resolveBillingAddress: function () { + return true; + } + }, 'Magento_Checkout/js/model/quote': { billingAddress: ko.observable(), shippingAddress: ko.observable(), diff --git a/dev/tests/static/framework/Magento/ruleset.xml b/dev/tests/static/framework/Magento/ruleset.xml index 56a5a9e55c30e..fac0842214d92 100644 --- a/dev/tests/static/framework/Magento/ruleset.xml +++ b/dev/tests/static/framework/Magento/ruleset.xml @@ -25,5 +25,6 @@ + diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php index 6fc84486c626b..2f1ab7a75bc83 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php @@ -452,7 +452,7 @@ private function convertModuleToPackageName($moduleName) { list($vendor, $name) = explode('_', $moduleName, 2); $package = 'module'; - foreach (preg_split('/([A-Z][a-z\d]+)/', $name, -1, PREG_SPLIT_DELIM_CAPTURE) as $chunk) { + foreach (preg_split('/([A-Z\d][a-z]*)/', $name, -1, PREG_SPLIT_DELIM_CAPTURE) as $chunk) { $package .= $chunk ? "-{$chunk}" : ''; } return strtolower("{$vendor}/{$package}"); diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml index 4790ce24fd87f..62406b6cbd30a 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml @@ -69,5 +69,8 @@ dev/build/publication/sanity/ce.xml + + dev/composer.lock + diff --git a/lib/internal/Magento/Framework/App/DocRootLocator.php b/lib/internal/Magento/Framework/App/DocRootLocator.php index 6fb35c42f1330..d73baf8e4e742 100644 --- a/lib/internal/Magento/Framework/App/DocRootLocator.php +++ b/lib/internal/Magento/Framework/App/DocRootLocator.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\App; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadFactory; /** @@ -20,18 +22,26 @@ class DocRootLocator private $request; /** + * @deprecated * @var ReadFactory */ private $readFactory; + /** + * @var Filesystem + */ + private $filesystem; + /** * @param RequestInterface $request * @param ReadFactory $readFactory + * @param Filesystem|null $filesystem */ - public function __construct(RequestInterface $request, ReadFactory $readFactory) + public function __construct(RequestInterface $request, ReadFactory $readFactory, Filesystem $filesystem = null) { $this->request = $request; $this->readFactory = $readFactory; + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); } /** @@ -42,7 +52,8 @@ public function __construct(RequestInterface $request, ReadFactory $readFactory) public function isPub() { $rootBasePath = $this->request->getServer('DOCUMENT_ROOT'); - $readDirectory = $this->readFactory->create(DirectoryList::ROOT); - return (substr($rootBasePath, -strlen('/pub')) === '/pub') && !$readDirectory->isExist($rootBasePath . 'setup'); + $readDirectory = $this->filesystem->getDirectoryRead(DirectoryList::ROOT); + + return (substr($rootBasePath, -\strlen('/pub')) === '/pub') && ! $readDirectory->isExist('setup'); } } diff --git a/lib/internal/Magento/Framework/App/Http.php b/lib/internal/Magento/Framework/App/Http.php index 3c6dee49f97b4..23024a44c2def 100644 --- a/lib/internal/Magento/Framework/App/Http.php +++ b/lib/internal/Magento/Framework/App/Http.php @@ -6,6 +6,7 @@ namespace Magento\Framework\App; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Debug; use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Framework\App\Request\Http as RequestHttp; use Magento\Framework\App\Response\Http as ResponseHttp; @@ -79,7 +80,7 @@ class Http implements \Magento\Framework\AppInterface * @param ResponseHttp $response * @param ConfigLoaderInterface $configLoader * @param State $state - * @param Filesystem $filesystem, + * @param Filesystem $filesystem * @param \Magento\Framework\Registry $registry */ public function __construct( @@ -149,7 +150,7 @@ public function launch() } /** - * {@inheritdoc} + * @inheritdoc */ public function catchException(Bootstrap $bootstrap, \Exception $exception) { @@ -198,6 +199,7 @@ private function buildContentFromException(\Exception $exception) { /** @var \Exception[] $exceptions */ $exceptions = []; + do { $exceptions[] = $exception; } while ($exception = $exception->getPrevious()); @@ -214,7 +216,12 @@ private function buildContentFromException(\Exception $exception) $index, get_class($exception), $exception->getMessage(), - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); } @@ -312,7 +319,15 @@ private function handleInitException(\Exception $exception) */ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) { - $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; + $reportData = [ + $exception->getMessage(), + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ]; $params = $bootstrap->getParams(); if (isset($params['REQUEST_URI'])) { $reportData['url'] = $params['REQUEST_URI']; diff --git a/lib/internal/Magento/Framework/App/StaticResource.php b/lib/internal/Magento/Framework/App/StaticResource.php index 87a2c37f94768..b5e4b2828d93b 100644 --- a/lib/internal/Magento/Framework/App/StaticResource.php +++ b/lib/internal/Magento/Framework/App/StaticResource.php @@ -10,6 +10,7 @@ use Magento\Framework\Filesystem; use Magento\Framework\Config\ConfigOptionsListConstants; use Psr\Log\LoggerInterface; +use Magento\Framework\Debug; /** * Entry point for retrieving static resources like JS, CSS, images by requested public path @@ -54,12 +55,12 @@ class StaticResource implements \Magento\Framework\AppInterface private $objectManager; /** - * @var \Magento\Framework\ObjectManager\ConfigLoaderInterface + * @var ConfigLoaderInterface */ private $configLoader; /** - * @var \Magento\Framework\Filesystem + * @var Filesystem */ private $filesystem; @@ -69,7 +70,7 @@ class StaticResource implements \Magento\Framework\AppInterface private $deploymentConfig; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ private $logger; @@ -138,7 +139,7 @@ public function launch() } /** - * {@inheritdoc} + * @inheritdoc */ public function catchException(Bootstrap $bootstrap, \Exception $exception) { @@ -146,7 +147,15 @@ public function catchException(Bootstrap $bootstrap, \Exception $exception) if ($bootstrap->isDeveloperMode()) { $this->response->setHttpResponseCode(404); $this->response->setHeader('Content-Type', 'text/plain'); - $this->response->setBody($exception->getMessage() . "\n" . $exception->getTraceAsString()); + $this->response->setBody( + $exception->getMessage() . "\n" . + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ); $this->response->sendResponse(); } else { require $this->getFilesystem()->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php index 23afbbc73d2b9..ef4152ba2e49e 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php @@ -8,6 +8,9 @@ use Magento\Framework\App\DocRootLocator; +/** + * Test for Magento\Framework\App\DocRootLocator class. + */ class DocRootLocatorTest extends \PHPUnit\Framework\TestCase { /** @@ -21,11 +24,15 @@ public function testIsPub($path, $isExist, $result) { $request = $this->createMock(\Magento\Framework\App\Request\Http::class); $request->expects($this->once())->method('getServer')->willReturn($path); + + $readFactory = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadFactory::class); + $reader = $this->createMock(\Magento\Framework\Filesystem\Directory\Read::class); + $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); + $filesystem->expects($this->once())->method('getDirectoryRead')->willReturn($reader); $reader->expects($this->any())->method('isExist')->willReturn($isExist); - $readFactory = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadFactory::class); - $readFactory->expects($this->once())->method('create')->willReturn($reader); - $model = new DocRootLocator($request, $readFactory); + + $model = new DocRootLocator($request, $readFactory, $filesystem); $this->assertSame($result, $model->isPub()); } diff --git a/lib/internal/Magento/Framework/Code/Generator/Autoloader.php b/lib/internal/Magento/Framework/Code/Generator/Autoloader.php index c214008393609..f8e469fe05265 100644 --- a/lib/internal/Magento/Framework/Code/Generator/Autoloader.php +++ b/lib/internal/Magento/Framework/Code/Generator/Autoloader.php @@ -3,37 +3,94 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Code\Generator; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Code\Generator; +use Psr\Log\LoggerInterface; +/** + * Class loader and generator. + */ class Autoloader { /** - * @var \Magento\Framework\Code\Generator + * @var Generator */ protected $_generator; /** - * @param \Magento\Framework\Code\Generator $generator + * Enables guarding against spamming the debug log with duplicate messages, as + * the generation exception will be thrown multiple times within a single request. + * + * @var string + */ + private $lastGenerationErrorMessage; + + /** + * @param Generator $generator */ - public function __construct( - \Magento\Framework\Code\Generator $generator - ) { + public function __construct(Generator $generator) + { $this->_generator = $generator; } /** * Load specified class name and generate it if necessary * + * According to PSR-4 section 2.4 an autoloader MUST NOT throw an exception and SHOULD NOT return a value. + * + * @see https://www.php-fig.org/psr/psr-4/ + * * @param string $className - * @return bool True if class was loaded + * @return void */ public function load($className) { - if (!class_exists($className)) { - return Generator::GENERATION_ERROR != $this->_generator->generateClass($className); + if (! class_exists($className)) { + try { + $this->_generator->generateClass($className); + } catch (\Exception $exception) { + $this->tryToLogExceptionMessageIfNotDuplicate($exception); + } + } + } + + /** + * Log exception. + * + * @param \Exception $exception + */ + private function tryToLogExceptionMessageIfNotDuplicate(\Exception $exception) + { + if ($this->lastGenerationErrorMessage !== $exception->getMessage()) { + $this->lastGenerationErrorMessage = $exception->getMessage(); + $this->tryToLogException($exception); + } + } + + /** + * Try to capture the exception message. + * + * The Autoloader is instantiated before the ObjectManager, so the LoggerInterface can not be injected. + * The Logger is instantiated in the try/catch block because ObjectManager might still not be initialized. + * In that case the exception message can not be captured. + * + * The debug level is used for logging in case class generation fails for a common class, but a custom + * autoloader is used later in the stack. A more severe log level would fill the logs with messages on production. + * The exception message now can be accessed in developer mode if debug logging is enabled. + * + * @param \Exception $exception + * @return void + */ + private function tryToLogException(\Exception $exception) + { + try { + $logger = ObjectManager::getInstance()->get(LoggerInterface::class); + $logger->debug($exception->getMessage(), ['exception' => $exception]); + } catch (\Exception $ignoreThisException) { + // Do not take an action here, since the original exception might have been caused by logger } - return true; } } diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 3689aeef81e0b..fa468a6df2909 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -2906,7 +2906,7 @@ public function prepareSqlCondition($fieldName, $condition) if (isset($condition['to'])) { $query .= empty($query) ? '' : ' AND '; $to = $this->_prepareSqlDateCondition($condition, 'to'); - $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); + $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); } } elseif (array_key_exists($key, $conditionKeyMap)) { $value = $condition[$key]; diff --git a/lib/internal/Magento/Framework/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index 4e9132d49a2e2..099753ac1b56f 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -7,7 +7,6 @@ use Magento\Framework\Data\Collection\EntityFactoryInterface; use Magento\Framework\Option\ArrayInterface; -use Magento\Framework\Exception\InputException; /** * Data collection @@ -235,20 +234,12 @@ protected function _setIsLoaded($flag = true) * Get current collection page * * @param int $displacement - * @throws \Magento\Framework\Exception\InputException * @return int */ public function getCurPage($displacement = 0) { if ($this->_curPage + $displacement < 1) { return 1; - } elseif ($this->_curPage > $this->getLastPageNumber() && $displacement === 0) { - throw new InputException( - __( - 'currentPage value %1 specified is greater than the %2 page(s) available.', - [$this->_curPage, $this->getLastPageNumber()] - ) - ); } elseif ($this->_curPage + $displacement > $this->getLastPageNumber()) { return $this->getLastPageNumber(); } else { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php index ac9cdd3822ec5..1b7e9ad990ce4 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php @@ -170,7 +170,11 @@ public function setId($id) */ public function getHtmlId() { - return $this->getForm()->getHtmlIdPrefix() . $this->getData('html_id') . $this->getForm()->getHtmlIdSuffix(); + return $this->_escaper->escapeHtml( + $this->getForm()->getHtmlIdPrefix() . + $this->getData('html_id') . + $this->getForm()->getHtmlIdSuffix() + ); } /** @@ -184,7 +188,7 @@ public function getName() if ($suffix = $this->getForm()->getFieldNameSuffix()) { $name = $this->getForm()->addSuffixToName($name, $suffix); } - return $name; + return $this->_escaper->escapeHtml($name); } /** diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Date.php b/lib/internal/Magento/Framework/Data/Form/Element/Date.php index e762a641bfdcc..52d3eae7d66ac 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Date.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Date.php @@ -49,6 +49,19 @@ public function __construct( } } + /** + * Check if a string is a date value + * + * @param string $value + * @return bool + */ + private function isDate(string $value): bool + { + $date = date_parse($value); + + return !empty($date['year']) && !empty($date['month']) && !empty($date['day']); + } + /** * If script executes on x64 system, converts large * numeric values to timestamp limit @@ -85,9 +98,10 @@ public function setValue($value) try { if (preg_match('/^[0-9]+$/', $value)) { $this->_value = (new \DateTime())->setTimestamp($this->_toTimestamp($value)); + } else if (is_string($value) && $this->isDate($value)) { + $this->_value = new \DateTime($value, new \DateTimeZone($this->localeDate->getConfigTimezone())); } else { - $this->_value = new \DateTime($value); - $this->_value->setTimezone(new \DateTimeZone($this->localeDate->getConfigTimezone())); + $this->_value = ''; } } catch (\Exception $e) { $this->_value = ''; diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Label.php b/lib/internal/Magento/Framework/Data/Form/Element/Label.php index 901dcb5289e8d..70b7885e7a0d0 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Label.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Label.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Data form abstract class - * - * @author Magento Core Team - */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\Phrase; + +/** + * Label form element. + */ class Label extends \Magento\Framework\Data\Form\Element\AbstractElement { /** @@ -37,8 +37,13 @@ public function __construct( public function getElementHtml() { $html = $this->getBold() ? '
' : '
'; - $html .= $this->getEscapedValue() . '
'; + if (is_string($this->getValue()) || $this->getValue() instanceof Phrase) { + $html .= $this->getEscapedValue(); + } + + $html .= '
'; $html .= $this->getAfterElementHtml(); + return $html; } } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php index 0b6ddd6999d3e..a69f5af08a93c 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php @@ -146,19 +146,6 @@ public function testGetCurPage() $this->assertEquals(1, $this->_model->getCurPage()); } - /** - * Test for getCurPage with exception. - * - * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage currentPage value 10 specified is greater than the 1 page(s) available. - * @return void - */ - public function testGetCurPageWithException() - { - $this->_model->setCurPage(10); - $this->_model->getCurPage(); - } - /** * Test for method possibleFlowWithItem. * diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php index a85c1f4aa450c..a207f45cb805a 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php @@ -38,6 +38,7 @@ protected function setUp() $this->_collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); $this->_escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->_escaperMock->method('escapeHtml')->willReturnArgument(0); $this->_model = $this->getMockForAbstractClass( \Magento\Framework\Data\Form\Element\AbstractElement::class, @@ -423,9 +424,6 @@ public function testGetHtmlContainerIdWithFieldContainerIdPrefix() */ public function testAddElementValues(array $initialData, $expectedValue) { - $this->_escaperMock->expects($this->any()) - ->method('escapeHtml') - ->will($this->returnArgument(0)); $this->_model->setValues($initialData['initial_values']); $this->_model->addElementValues($initialData['add_values'], $initialData['overwrite']); diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php index a2a40ee03b044..d347fed13ed65 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php @@ -26,6 +26,7 @@ protected function setUp() $factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $escaperMock->method('escapeHtml')->willReturnArgument(0); $this->_link = new \Magento\Framework\Data\Form\Element\Link( $factoryMock, $collectionFactoryMock, diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php index 6d1680a9f38a6..ed2b04e47b7a0 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php @@ -7,6 +7,11 @@ class MultiselectTest extends \PHPUnit\Framework\TestCase { + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + /** * @var \Magento\Framework\Data\Form\Element\Multiselect */ @@ -15,7 +20,12 @@ class MultiselectTest extends \PHPUnit\Framework\TestCase protected function setUp() { $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_model = $testHelper->getObject(\Magento\Framework\Data\Form\Element\Editablemultiselect::class); + $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->escaperMock->method('escapeHtml')->willReturnArgument(0); + $this->_model = $testHelper->getObject( + \Magento\Framework\Data\Form\Element\Editablemultiselect::class, + ['escaper' => $this->escaperMock] + ); $this->_model->setForm(new \Magento\Framework\DataObject()); } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php index f77f4a816a1af..d58bc8639e82f 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php @@ -26,6 +26,7 @@ protected function setUp() $factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $escaperMock->method('escapeHtml')->willReturnArgument(0); $this->_model = new \Magento\Framework\Data\Form\Element\Note( $factoryMock, $collectionFactoryMock, diff --git a/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php b/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php index a7a387b5def81..acd0a61633557 100644 --- a/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php +++ b/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php @@ -9,7 +9,12 @@ namespace Magento\Framework\Event\Invoker; use Magento\Framework\Event\Observer; +use Psr\Log\LoggerInterface; +use Magento\Framework\App\State; +/** + * Default Invoker. + */ class InvokerDefault implements \Magento\Framework\Event\InvokerInterface { /** @@ -22,20 +27,29 @@ class InvokerDefault implements \Magento\Framework\Event\InvokerInterface /** * Application state * - * @var \Magento\Framework\App\State + * @var State */ protected $_appState; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param \Magento\Framework\Event\ObserverFactory $observerFactory - * @param \Magento\Framework\App\State $appState + * @param State $appState + * @param LoggerInterface $logger */ public function __construct( \Magento\Framework\Event\ObserverFactory $observerFactory, - \Magento\Framework\App\State $appState + State $appState, + LoggerInterface $logger = null ) { $this->_observerFactory = $observerFactory; $this->_appState = $appState; + $this->logger = $logger ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(LoggerInterface::class); } /** @@ -61,6 +75,8 @@ public function dispatch(array $configuration, Observer $observer) } /** + * Execute Observer. + * * @param \Magento\Framework\Event\ObserverInterface $object * @param Observer $observer * @return $this @@ -70,7 +86,7 @@ protected function _callObserverMethod($object, $observer) { if ($object instanceof \Magento\Framework\Event\ObserverInterface) { $object->execute($observer); - } elseif ($this->_appState->getMode() == \Magento\Framework\App\State::MODE_DEVELOPER) { + } elseif ($this->_appState->getMode() == State::MODE_DEVELOPER) { throw new \LogicException( sprintf( 'Observer "%s" must implement interface "%s"', @@ -78,6 +94,12 @@ protected function _callObserverMethod($object, $observer) \Magento\Framework\Event\ObserverInterface::class ) ); + } else { + $this->logger->warning(sprintf( + 'Observer "%s" must implement interface "%s"', + get_class($object), + \Magento\Framework\Event\ObserverInterface::class + )); } return $this; } diff --git a/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php b/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php index 37f650dbef6a0..e6ec123823854 100644 --- a/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php +++ b/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Event\Test\Unit\Invoker; +/** + * Test for Magento\Framework\Event\Invoker\InvokerDefault. + */ class InvokerDefaultTest extends \PHPUnit\Framework\TestCase { /** @@ -32,6 +35,11 @@ class InvokerDefaultTest extends \PHPUnit\Framework\TestCase */ protected $_invokerDefault; + /** + * @var |Psr\Log|LoggerInterface + */ + private $loggerMock; + protected function setUp() { $this->_observerFactoryMock = $this->createMock(\Magento\Framework\Event\ObserverFactory::class); @@ -41,10 +49,12 @@ protected function setUp() ['execute'] ); $this->_appStateMock = $this->createMock(\Magento\Framework\App\State::class); + $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); $this->_invokerDefault = new \Magento\Framework\Event\Invoker\InvokerDefault( $this->_observerFactoryMock, - $this->_appStateMock + $this->_appStateMock, + $this->loggerMock ); } @@ -166,13 +176,15 @@ public function testWrongInterfaceCallWithDisabledDeveloperMode($shared) $this->returnValue($notObserver) ); $this->_appStateMock->expects( - $this->once() + $this->exactly(1) )->method( 'getMode' )->will( $this->returnValue(\Magento\Framework\App\State::MODE_PRODUCTION) ); + $this->loggerMock->expects($this->once())->method('warning'); + $this->_invokerDefault->dispatch( [ 'shared' => $shared, diff --git a/lib/internal/Magento/Framework/Filter/Template.php b/lib/internal/Magento/Framework/Filter/Template.php index a2e772853efcb..62a00b3dc0123 100644 --- a/lib/internal/Magento/Framework/Filter/Template.php +++ b/lib/internal/Magento/Framework/Filter/Template.php @@ -9,6 +9,9 @@ */ namespace Magento\Framework\Filter; +use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\Model\AbstractModel; + /** * @api */ @@ -52,6 +55,18 @@ class Template implements \Zend_Filter_Interface */ protected $string; + /** + * @var string[] + */ + private $restrictedMethods = [ + 'addafterfiltercallback', + 'getresourcecollection', + 'load', + 'save', + 'getcollection', + 'getresource' + ]; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param array $variables @@ -297,6 +312,46 @@ protected function getParameters($value) return $params; } + /** + * Validate method call initiated in a template. + * + * Deny calls for methods that may disrupt template processing. + * + * @param object $object + * @param string $method + * @return void + * @throws \InvalidArgumentException + */ + private function validateVariableMethodCall($object, string $method) + { + if ($object === $this) { + if (in_array(mb_strtolower($method), $this->restrictedMethods)) { + throw new \InvalidArgumentException("Method $method cannot be called from template."); + } + } + } + + /** + * Check allowed methods for data objects. + * + * Deny calls for methods that may disrupt template processing. + * + * @param object $object + * @param string $method + * @return bool + * @throws \InvalidArgumentException + */ + private function isAllowedDataObjectMethod($object, string $method): bool + { + if ($object instanceof AbstractExtensibleModel || $object instanceof AbstractModel) { + if (in_array(mb_strtolower($method), $this->restrictedMethods)) { + throw new \InvalidArgumentException("Method $method cannot be called from template."); + } + } + + return true; + } + /** * Return variable value for var construction * @@ -336,21 +391,27 @@ protected function getVariable($value, $default = '{no_value_defined}') || substr($stackVars[$i]['name'], 0, 3) == 'get' ) { $stackVars[$i]['args'] = $this->getStackArgs($stackVars[$i]['args']); - $stackVars[$i]['variable'] = call_user_func_array( - [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], - $stackVars[$i]['args'] - ); + + if ($this->isAllowedDataObjectMethod($stackVars[$i - 1]['variable'], $stackVars[$i]['name'])) { + $stackVars[$i]['variable'] = call_user_func_array( + [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], + $stackVars[$i]['args'] + ); + } } } $last = $i; - } elseif (isset($stackVars[$i - 1]['variable']) && $stackVars[$i]['type'] == 'method') { + } elseif (isset($stackVars[$i - 1]['variable']) + && is_object($stackVars[$i - 1]['variable']) + && $stackVars[$i]['type'] == 'method' + ) { // Calling object methods - if (method_exists($stackVars[$i - 1]['variable'], $stackVars[$i]['name'])) { - $stackVars[$i]['args'] = $this->getStackArgs($stackVars[$i]['args']); - $stackVars[$i]['variable'] = call_user_func_array( - [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], - $stackVars[$i]['args'] - ); + $object = $stackVars[$i - 1]['variable']; + $method = $stackVars[$i]['name']; + if (method_exists($object, $method)) { + $args = $this->getStackArgs($stackVars[$i]['args']); + $this->validateVariableMethodCall($object, $method); + $stackVars[$i]['variable'] = call_user_func_array([$object, $method], $args); } $last = $i; } diff --git a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php index e7376d9b7d264..a78c14eabc587 100644 --- a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php +++ b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php @@ -6,6 +6,8 @@ namespace Magento\Framework\Filter\Test\Unit; +use Magento\Store\Model\Store; + class TemplateTest extends \PHPUnit\Framework\TestCase { /** @@ -13,10 +15,16 @@ class TemplateTest extends \PHPUnit\Framework\TestCase */ private $templateFilter; + /** + * @var Store + */ + private $store; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->templateFilter = $objectManager->getObject(\Magento\Framework\Filter\Template::class); + $this->store = $objectManager->getObject(Store::class); } public function testFilter() @@ -205,4 +213,60 @@ public function varDirectiveDataProvider() ], ]; } + + /** + * Test adding callbacks when already filtering. + * + * @expectedException \InvalidArgumentException + */ + public function testInappropriateCallbacks() + { + $this->templateFilter->setVariables(['filter' => $this->templateFilter]); + $this->templateFilter->filter('Test {{var filter.addAfterFilterCallback(\'mb_strtolower\')}}'); + } + + /** + * Test adding callbacks when already filtering. + * + * @param string $method + * @dataProvider disallowedMethods + * @expectedException \InvalidArgumentException + * + * @return void + */ + public function testDisallowedMethods(string $method) + { + $this->templateFilter->setVariables(['store' => $this->store]); + $this->templateFilter->filter('{{var store.'.$method.'()}}'); + } + + /** + * Data for testDisallowedMethods method. + * + * @return array + */ + public function disallowedMethods(): array + { + return [ + ['getResourceCollection'], + ['load'], + ['save'], + ['getCollection'], + ['getResource'], + ]; + } + + /** + * Check that if calling a method of an object fails expected result is returned. + * + * @return void + */ + public function testInvalidMethodCall() + { + $this->templateFilter->setVariables(['dateTime' => '\DateTime']); + $this->assertEquals( + '\DateTime', + $this->templateFilter->filter('{{var dateTime.createFromFormat(\'d\',\'1548201468\')}}') + ); + } } diff --git a/lib/internal/Magento/Framework/Locale/Format.php b/lib/internal/Magento/Framework/Locale/Format.php index ca50cdb2440f4..adcffe01b910e 100644 --- a/lib/internal/Magento/Framework/Locale/Format.php +++ b/lib/internal/Magento/Framework/Locale/Format.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Locale; +/** + * Price locale format. + */ class Format implements \Magento\Framework\Locale\FormatInterface { /** @@ -38,7 +41,8 @@ public function __construct( } /** - * Returns the first found number from a string + * Returns the first found number from a string. + * * Parsing depends on given locale (grouping and decimal) * * Examples for input: @@ -100,7 +104,7 @@ public function getPriceFormat($localeCode = null, $currencyCode = null) } $formatter = new \NumberFormatter( - $localeCode . '@currency=' . $currency->getCode(), + $currency->getCode() ? $localeCode . '@currency=' . $currency->getCode() : $localeCode, \NumberFormatter::CURRENCY ); $format = $formatter->getPattern(); diff --git a/lib/internal/Magento/Framework/Locale/Resolver.php b/lib/internal/Magento/Framework/Locale/Resolver.php index 8372908a380ff..abbfbdc5c6c37 100644 --- a/lib/internal/Magento/Framework/Locale/Resolver.php +++ b/lib/internal/Magento/Framework/Locale/Resolver.php @@ -6,7 +6,12 @@ namespace Magento\Framework\Locale; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +/** + * Manages locale config information. + */ class Resolver implements ResolverInterface { /** @@ -47,26 +52,34 @@ class Resolver implements ResolverInterface */ protected $emulatedLocales = []; + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + /** * @param ScopeConfigInterface $scopeConfig * @param string $defaultLocalePath * @param string $scopeType * @param mixed $locale + * @param DeploymentConfig|null $deploymentConfig */ public function __construct( ScopeConfigInterface $scopeConfig, $defaultLocalePath, $scopeType, - $locale = null + $locale = null, + DeploymentConfig $deploymentConfig = null ) { $this->scopeConfig = $scopeConfig; $this->defaultLocalePath = $defaultLocalePath; $this->scopeType = $scopeType; + $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->create(DeploymentConfig::class); $this->setLocale($locale); } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultLocalePath() { @@ -74,7 +87,7 @@ public function getDefaultLocalePath() } /** - * {@inheritdoc} + * @inheritdoc */ public function setDefaultLocale($locale) { @@ -83,12 +96,15 @@ public function setDefaultLocale($locale) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultLocale() { if (!$this->defaultLocale) { - $locale = $this->scopeConfig->getValue($this->getDefaultLocalePath(), $this->scopeType); + $locale = false; + if ($this->deploymentConfig->isAvailable() && $this->deploymentConfig->isDbAvailable()) { + $locale = $this->scopeConfig->getValue($this->getDefaultLocalePath(), $this->scopeType); + } if (!$locale) { $locale = self::DEFAULT_LOCALE; } @@ -98,7 +114,7 @@ public function getDefaultLocale() } /** - * {@inheritdoc} + * @inheritdoc */ public function setLocale($locale = null) { @@ -111,7 +127,7 @@ public function setLocale($locale = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getLocale() { @@ -122,7 +138,7 @@ public function getLocale() } /** - * {@inheritdoc} + * @inheritdoc */ public function emulate($scopeId) { @@ -142,7 +158,7 @@ public function emulate($scopeId) } /** - * {@inheritdoc} + * @inheritdoc */ public function revert() { diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php index aa7ca377efa03..f6d7326f52764 100644 --- a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php @@ -53,6 +53,7 @@ protected function setUp() /** @var \Magento\Directory\Model\CurrencyFactory|\PHPUnit_Framework_MockObject_MockObject $currencyFactory */ $currencyFactory = $this->getMockBuilder(\Magento\Directory\Model\CurrencyFactory::class) + ->disableOriginalConstructor() ->getMock(); $this->formatModel = new \Magento\Framework\Locale\Format( diff --git a/lib/internal/Magento/Framework/Lock/Backend/FileLock.php b/lib/internal/Magento/Framework/Lock/Backend/FileLock.php new file mode 100644 index 0000000000000..d168e910a4ab7 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/FileLock.php @@ -0,0 +1,194 @@ +fileDriver = $fileDriver; + $this->path = rtrim($path, '/') . '/'; + + try { + if (!$this->fileDriver->isExists($this->path)) { + $this->fileDriver->createDirectory($this->path); + } + } catch (FileSystemException $exception) { + throw new RuntimeException( + new Phrase('Cannot create the directory for locks: %1', [$this->path]), + $exception + ); + } + } + + /** + * Acquires a lock by name + * + * @param string $name The lock name + * @param int $timeout Timeout in seconds. A negative timeout value means infinite timeout + * @return bool Returns true if the lock is acquired, otherwise returns false + * @throws RuntimeException Throws RuntimeException if cannot acquires the lock because FS problems + */ + public function lock(string $name, int $timeout = -1): bool + { + try { + $lockFile = $this->getLockPath($name); + $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); + $skipDeadline = $timeout < 0; + $deadline = microtime(true) + $timeout; + + while (!$this->tryToLock($fileResource)) { + if (!$skipDeadline && $deadline <= microtime(true)) { + $this->fileDriver->fileClose($fileResource); + return false; + } + usleep($this->sleepCycle); + } + } catch (FileSystemException $exception) { + throw new RuntimeException(new Phrase('Cannot acquire a lock.'), $exception); + } + + $this->locks[$lockFile] = $fileResource; + return true; + } + + /** + * Checks if a lock exists by name + * + * @param string $name The lock name + * @return bool Returns true if the lock exists, otherwise returns false + * @throws RuntimeException Throws RuntimeException if cannot check that the lock exists + */ + public function isLocked(string $name): bool + { + $lockFile = $this->getLockPath($name); + $result = false; + + try { + if ($this->fileDriver->isExists($lockFile)) { + $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); + if ($this->tryToLock($fileResource)) { + $result = false; + } else { + $result = true; + } + $this->fileDriver->fileClose($fileResource); + } + } catch (FileSystemException $exception) { + throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception); + } + + return $result; + } + + /** + * Remove the lock by name + * + * @param string $name The lock name + * @return bool If the lock is removed returns true, otherwise returns false + */ + public function unlock(string $name): bool + { + $lockFile = $this->getLockPath($name); + + if (isset($this->locks[$lockFile]) && $this->tryToUnlock($this->locks[$lockFile])) { + unset($this->locks[$lockFile]); + return true; + } + + return false; + } + + /** + * Returns the full path to the lock file by name + * + * @param string $name The lock name + * @return string The path to the lock file + */ + private function getLockPath(string $name): string + { + return $this->path . $name; + } + + /** + * Tries to lock a file resource + * + * @param resource $resource The file resource + * @return bool If the lock is acquired returns true, otherwise returns false + */ + private function tryToLock($resource): bool + { + try { + return $this->fileDriver->fileLock($resource, LOCK_EX | LOCK_NB); + } catch (FileSystemException $exception) { + return false; + } + } + + /** + * Tries to unlock a file resource + * + * @param resource $resource The file resource + * @return bool If the lock is removed returns true, otherwise returns false + */ + private function tryToUnlock($resource): bool + { + try { + return $this->fileDriver->fileLock($resource, LOCK_UN | LOCK_NB); + } catch (FileSystemException $exception) { + return false; + } + } +} diff --git a/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php b/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php new file mode 100644 index 0000000000000..cbba981ae1b51 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php @@ -0,0 +1,280 @@ +\Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']]; + + /** + * The mapping list of the lock name with the full lock path + * + * @var array + */ + private $locks = []; + + /** + * The default path to storage locks + */ + const DEFAULT_PATH = '/magento/locks'; + + /** + * @param string $host The host to connect to Zookeeper + * @param string $path The base path to locks in Zookeeper + * @throws RuntimeException + */ + public function __construct(string $host, string $path = self::DEFAULT_PATH) + { + if (!$path) { + throw new RuntimeException( + new Phrase('The path needs to be a non-empty string.') + ); + } + + if (!$host) { + throw new RuntimeException( + new Phrase('The host needs to be a non-empty string.') + ); + } + + $this->host = $host; + $this->path = rtrim($path, '/') . '/'; + } + + /** + * @inheritdoc + * + * You can see the lock algorithm by the link + * @link https://zookeeper.apache.org/doc/r3.1.2/recipes.html#sc_recipes_Locks + * + * @throws RuntimeException + */ + public function lock(string $name, int $timeout = -1): bool + { + $skipDeadline = $timeout < 0; + $lockPath = $this->getFullPathToLock($name); + $deadline = microtime(true) + $timeout; + + if (!$this->checkAndCreateParentNode($lockPath)) { + throw new RuntimeException(new Phrase('Failed creating the path %1', [$lockPath])); + } + + $lockKey = $this->getProvider() + ->create($lockPath, '1', $this->acl, \Zookeeper::EPHEMERAL | \Zookeeper::SEQUENCE); + + if (!$lockKey) { + throw new RuntimeException(new Phrase('Failed creating lock %1', [$lockPath])); + } + + while ($this->isAnyLock($lockKey, $this->getIndex($lockKey))) { + if (!$skipDeadline && $deadline <= microtime(true)) { + $this->getProvider()->delete($lockKey); + return false; + } + + usleep($this->sleepCycle); + } + + $this->locks[$name] = $lockKey; + + return true; + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function unlock(string $name): bool + { + if (!isset($this->locks[$name])) { + return false; + } + + return $this->getProvider()->delete($this->locks[$name]); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function isLocked(string $name): bool + { + return $this->isAnyLock($this->getFullPathToLock($name)); + } + + /** + * Gets full path to lock by its name + * + * @param string $name + * @return string + */ + private function getFullPathToLock(string $name): string + { + return $this->path . $name . '/' . $this->lockName; + } + + /** + * Initiolizes and returns Zookeeper provider + * + * @return \Zookeeper + * @throws RuntimeException + */ + private function getProvider(): \Zookeeper + { + if (!$this->zookeeper) { + $this->zookeeper = new \Zookeeper($this->host); + } + + $deadline = microtime(true) + $this->connectionTimeout; + while ($this->zookeeper->getState() != \Zookeeper::CONNECTED_STATE) { + if ($deadline <= microtime(true)) { + throw new RuntimeException(new Phrase('Zookeeper connection timed out!')); + } + usleep($this->sleepCycle); + } + + return $this->zookeeper; + } + + /** + * Checks and creates base path recursively + * + * @param string $path + * @return bool + * @throws RuntimeException + */ + private function checkAndCreateParentNode(string $path): bool + { + $path = dirname($path); + if ($this->getProvider()->exists($path)) { + return true; + } + + if (!$this->checkAndCreateParentNode($path)) { + return false; + } + + if ($this->getProvider()->create($path, '1', $this->acl)) { + return true; + } + + return $this->getProvider()->exists($path); + } + + /** + * Gets int increment of lock key + * + * @param string $key + * @return int|null + */ + private function getIndex(string $key) + { + if (!preg_match('/' . $this->lockName . '([0-9]+)$/', $key, $matches)) { + return null; + } + + return intval($matches[1]); + } + + /** + * Checks if there is any sequence node under parent of $fullKey. + * + * At first checks that the $fullKey node is present, if not - returns false. + * If $indexKey is non-null and there is a smaller index than $indexKey then returns true, + * otherwise returns false. + * + * @param string $fullKey The full path without any sequence info + * @param int|null $indexKey The index to compare + * @return bool + * @throws RuntimeException + */ + private function isAnyLock(string $fullKey, int $indexKey = null): bool + { + $parent = dirname($fullKey); + + if (!$this->getProvider()->exists($parent)) { + return false; + } + + $children = $this->getProvider()->getChildren($parent); + + if (null === $indexKey && !empty($children)) { + return true; + } + + foreach ($children as $childKey) { + $childIndex = $this->getIndex($childKey); + + if (null === $childIndex) { + continue; + } + + if ($childIndex < $indexKey) { + return true; + } + } + + return false; + } +} diff --git a/lib/internal/Magento/Framework/Lock/LockBackendFactory.php b/lib/internal/Magento/Framework/Lock/LockBackendFactory.php new file mode 100644 index 0000000000000..b142085ef6563 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/LockBackendFactory.php @@ -0,0 +1,111 @@ + DatabaseLock::class, + self::LOCK_ZOOKEEPER => ZookeeperLock::class, + self::LOCK_CACHE => CacheLock::class, + self::LOCK_FILE => FileLock::class, + ]; + + /** + * @param ObjectManagerInterface $objectManager The Object Manager instance + * @param DeploymentConfig $deploymentConfig The Application deployment configuration + */ + public function __construct( + ObjectManagerInterface $objectManager, + DeploymentConfig $deploymentConfig + ) { + $this->objectManager = $objectManager; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * Creates an instance of LockManagerInterface using information from deployment config + * + * @return LockManagerInterface + * @throws RuntimeException + */ + public function create(): LockManagerInterface + { + $provider = $this->deploymentConfig->get('lock/provider', self::LOCK_DB); + $config = $this->deploymentConfig->get('lock/config', []); + + if (!isset($this->lockers[$provider])) { + throw new RuntimeException(new Phrase('Unknown locks provider: %1', [$provider])); + } + + if (self::LOCK_ZOOKEEPER === $provider && !extension_loaded(self::LOCK_ZOOKEEPER)) { + throw new RuntimeException(new Phrase('php extension Zookeeper is not installed.')); + } + + return $this->objectManager->create($this->lockers[$provider], $config); + } +} diff --git a/lib/internal/Magento/Framework/Lock/LockManagerInterface.php b/lib/internal/Magento/Framework/Lock/LockManagerInterface.php index 9df65f45adac3..76cc8506eb182 100644 --- a/lib/internal/Magento/Framework/Lock/LockManagerInterface.php +++ b/lib/internal/Magento/Framework/Lock/LockManagerInterface.php @@ -3,8 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); + namespace Magento\Framework\Lock; /** diff --git a/lib/internal/Magento/Framework/Lock/Proxy.php b/lib/internal/Magento/Framework/Lock/Proxy.php new file mode 100644 index 0000000000000..2718bf6cb3456 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Proxy.php @@ -0,0 +1,83 @@ +factory = $factory; + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function isLocked(string $name): bool + { + return $this->getLocker()->isLocked($name); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function lock(string $name, int $timeout = -1): bool + { + return $this->getLocker()->lock($name, $timeout); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function unlock(string $name): bool + { + return $this->getLocker()->unlock($name); + } + + /** + * Gets LockManagerInterface implementation using Factory + * + * @return LockManagerInterface + * @throws RuntimeException + */ + private function getLocker(): LockManagerInterface + { + if (!$this->locker) { + $this->locker = $this->factory->create(); + } + + return $this->locker; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php new file mode 100644 index 0000000000000..62521b9de3082 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php @@ -0,0 +1,68 @@ +markTestSkipped('Test was skipped because php extension Zookeeper is not installed.'); + } + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage The path needs to be a non-empty string. + * @return void + */ + public function testConstructionWithPathException() + { + $this->zookeeperProvider = new ZookeeperProvider($this->host, ''); + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage The host needs to be a non-empty string. + * @return void + */ + public function testConstructionWithHostException() + { + $this->zookeeperProvider = new ZookeeperProvider('', $this->path); + } + + /** + * @return void + */ + public function testConstructionWithoutException() + { + $this->zookeeperProvider = new ZookeeperProvider($this->host, $this->path); + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php new file mode 100644 index 0000000000000..ebf2f54f3e093 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php @@ -0,0 +1,116 @@ +objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->factory = new LockBackendFactory($this->objectManagerMock, $this->deploymentConfigMock); + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage Unknown locks provider: someProvider + */ + public function testCreateWithException() + { + $this->deploymentConfigMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive(['lock/provider', LockBackendFactory::LOCK_DB], ['lock/config', []]) + ->willReturnOnConsecutiveCalls('someProvider', []); + + $this->factory->create(); + } + + /** + * @param string $lockProvider + * @param string $lockProviderClass + * @param array $config + * @dataProvider createDataProvider + */ + public function testCreate(string $lockProvider, string $lockProviderClass, array $config) + { + $lockManagerMock = $this->getMockForAbstractClass(LockManagerInterface::class); + $this->deploymentConfigMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive(['lock/provider', LockBackendFactory::LOCK_DB], ['lock/config', []]) + ->willReturnOnConsecutiveCalls($lockProvider, $config); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with($lockProviderClass, $config) + ->willReturn($lockManagerMock); + + $this->assertSame($lockManagerMock, $this->factory->create()); + } + + /** + * @return array + */ + public function createDataProvider(): array + { + $data = [ + 'db' => [ + 'lockProvider' => LockBackendFactory::LOCK_DB, + 'lockProviderClass' => DatabaseLock::class, + 'config' => ['prefix' => 'somePrefix'], + ], + 'cache' => [ + 'lockProvider' => LockBackendFactory::LOCK_CACHE, + 'lockProviderClass' => CacheLock::class, + 'config' => [], + ], + 'file' => [ + 'lockProvider' => LockBackendFactory::LOCK_FILE, + 'lockProviderClass' => FileLock::class, + 'config' => ['path' => '/my/path'], + ], + ]; + + if (extension_loaded('zookeeper')) { + $data['zookeeper'] = [ + 'lockProvider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'lockProviderClass' => ZookeeperLock::class, + 'config' => ['host' => 'some host'], + ]; + } + + return $data; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php new file mode 100644 index 0000000000000..c71dad701d715 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php @@ -0,0 +1,106 @@ +factoryMock = $this->createMock(LockBackendFactory::class); + $this->lockerMock = $this->getMockForAbstractClass(LockManagerInterface::class); + $this->proxy = new Proxy($this->factoryMock); + } + + /** + * @return void + */ + public function testIsLocked() + { + $lockName = 'testLock'; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('isLocked') + ->with($lockName) + ->willReturn(true); + + $this->assertTrue($this->proxy->isLocked($lockName)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->isLocked($lockName)); + } + + /** + * @return void + */ + public function testLock() + { + $lockName = 'testLock'; + $timeout = 123; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('lock') + ->with($lockName, $timeout) + ->willReturn(true); + + $this->assertTrue($this->proxy->lock($lockName, $timeout)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->lock($lockName, $timeout)); + } + + /** + * @return void + */ + public function testUnlock() + { + $lockName = 'testLock'; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('unlock') + ->with($lockName) + ->willReturn(true); + + $this->assertTrue($this->proxy->unlock($lockName)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->unlock($lockName)); + } +} diff --git a/lib/internal/Magento/Framework/Message/Manager.php b/lib/internal/Magento/Framework/Message/Manager.php index 4e73b6112f9d8..d71e196deea88 100644 --- a/lib/internal/Magento/Framework/Message/Manager.php +++ b/lib/internal/Magento/Framework/Message/Manager.php @@ -8,6 +8,7 @@ use Magento\Framework\Event; use Psr\Log\LoggerInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Debug; /** * Message manager model @@ -69,7 +70,7 @@ class Manager implements ManagerInterface * @param Event\ManagerInterface $eventManager * @param LoggerInterface $logger * @param string $defaultGroup - * @param ExceptionMessageFactoryInterface|null exceptionMessageFactory + * @param ExceptionMessageFactoryInterface|null $exceptionMessageFactory */ public function __construct( Session $session, @@ -91,7 +92,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultGroup() { @@ -112,8 +113,8 @@ protected function prepareGroup($group) /** * @inheritdoc * - * @param string|null $group * @param bool $clear + * @param string|null $group * @return Collection */ public function getMessages($clear = false, $group = null) @@ -250,7 +251,12 @@ public function addException(\Exception $exception, $alternativeText = null, $gr 'Exception message: %s%sTrace: %s', $exception->getMessage(), "\n", - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); $this->logger->critical($message); @@ -288,7 +294,12 @@ public function addExceptionMessage(\Exception $exception, $alternativeText = nu 'Exception message: %s%sTrace: %s', $exception->getMessage(), "\n", - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); $this->logger->critical($message); diff --git a/lib/internal/Magento/Framework/Session/SessionManager.php b/lib/internal/Magento/Framework/Session/SessionManager.php index 0d0838b47d9cf..11393674012f3 100644 --- a/lib/internal/Magento/Framework/Session/SessionManager.php +++ b/lib/internal/Magento/Framework/Session/SessionManager.php @@ -12,6 +12,7 @@ /** * Session Manager * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class SessionManager implements SessionManagerInterface { @@ -92,6 +93,11 @@ class SessionManager implements SessionManagerInterface */ private $appState; + /** + * @var SessionStartChecker + */ + private $sessionStartChecker; + /** * @param \Magento\Framework\App\Request\Http $request * @param SidResolverInterface $sidResolver @@ -102,7 +108,10 @@ class SessionManager implements SessionManagerInterface * @param \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager * @param \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory * @param \Magento\Framework\App\State $appState + * @param SessionStartChecker|null $sessionStartChecker * @throws \Magento\Framework\Exception\SessionException + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Request\Http $request, @@ -113,7 +122,8 @@ public function __construct( StorageInterface $storage, \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager, \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory, - \Magento\Framework\App\State $appState + \Magento\Framework\App\State $appState, + SessionStartChecker $sessionStartChecker = null ) { $this->request = $request; $this->sidResolver = $sidResolver; @@ -124,6 +134,9 @@ public function __construct( $this->cookieManager = $cookieManager; $this->cookieMetadataFactory = $cookieMetadataFactory; $this->appState = $appState; + $this->sessionStartChecker = $sessionStartChecker ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + SessionStartChecker::class + ); // Enable session.use_only_cookies ini_set('session.use_only_cookies', '1'); @@ -131,7 +144,8 @@ public function __construct( } /** - * This method needs to support sessions with APC enabled + * This method needs to support sessions with APC enabled. + * * @return void */ public function writeClose() @@ -166,47 +180,50 @@ public function __call($method, $args) */ public function start() { - if (!$this->isSessionExists()) { - \Magento\Framework\Profiler::start('session_start'); - - try { - $this->appState->getAreaCode(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - throw new \Magento\Framework\Exception\SessionException( - new \Magento\Framework\Phrase( - 'Area code not set: Area code must be set before starting a session.' - ), - $e - ); - } - - // Need to apply the config options so they can be ready by session_start - $this->initIniOptions(); - $this->registerSaveHandler(); - if (isset($_SESSION['new_session_id'])) { - // Not fully expired yet. Could be lost cookie by unstable network. - session_commit(); - session_id($_SESSION['new_session_id']); - } - $sid = $this->sidResolver->getSid($this); - // potential custom logic for session id (ex. switching between hosts) - $this->setSessionId($sid); - session_start(); - if (isset($_SESSION['destroyed']) - && $_SESSION['destroyed'] < time() - $this->sessionConfig->getCookieLifetime() - ) { - $this->destroy(['clear_storage' => true]); + if ($this->sessionStartChecker->check()) { + if (!$this->isSessionExists()) { + \Magento\Framework\Profiler::start('session_start'); + + try { + $this->appState->getAreaCode(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + throw new \Magento\Framework\Exception\SessionException( + new \Magento\Framework\Phrase( + 'Area code not set: Area code must be set before starting a session.' + ), + $e + ); + } + + // Need to apply the config options so they can be ready by session_start + $this->initIniOptions(); + $this->registerSaveHandler(); + if (isset($_SESSION['new_session_id'])) { + // Not fully expired yet. Could be lost cookie by unstable network. + session_commit(); + session_id($_SESSION['new_session_id']); + } + $sid = $this->sidResolver->getSid($this); + // potential custom logic for session id (ex. switching between hosts) + $this->setSessionId($sid); + session_start(); + if (isset($_SESSION['destroyed']) + && $_SESSION['destroyed'] < time() - $this->sessionConfig->getCookieLifetime() + ) { + $this->destroy(['clear_storage' => true]); + } + + $this->validator->validate($this); + $this->renewCookie($sid); + + register_shutdown_function([$this, 'writeClose']); + + $this->_addHost(); + \Magento\Framework\Profiler::stop('session_start'); } - - $this->validator->validate($this); - $this->renewCookie($sid); - - register_shutdown_function([$this, 'writeClose']); - - $this->_addHost(); - \Magento\Framework\Profiler::stop('session_start'); + $this->storage->init(isset($_SESSION) ? $_SESSION : []); } - $this->storage->init(isset($_SESSION) ? $_SESSION : []); + return $this; } diff --git a/lib/internal/Magento/Framework/Session/SessionStartChecker.php b/lib/internal/Magento/Framework/Session/SessionStartChecker.php new file mode 100644 index 0000000000000..9cc32268d574a --- /dev/null +++ b/lib/internal/Magento/Framework/Session/SessionStartChecker.php @@ -0,0 +1,38 @@ +checkSapi = $checkSapi; + } + + /** + * Can session be started or not. + * + * @return bool + */ + public function check() : bool + { + return !($this->checkSapi && PHP_SAPI === 'cli'); + } +} diff --git a/lib/internal/Magento/Framework/Validator/Factory.php b/lib/internal/Magento/Framework/Validator/Factory.php index f2089c662e955..2a296f7cdcb24 100644 --- a/lib/internal/Magento/Framework/Validator/Factory.php +++ b/lib/internal/Magento/Framework/Validator/Factory.php @@ -6,22 +6,33 @@ namespace Magento\Framework\Validator; +use Magento\Framework\Module\Dir\Reader; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Validator; use Magento\Framework\Cache\FrontendInterface; +/** + * Factory for \Magento\Framework\Validator and \Magento\Framework\Validator\Builder. + */ class Factory { - /** cache key */ + /** + * cache key + * + * @deprecated + */ const CACHE_KEY = __CLASS__; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $_objectManager; /** * Validator config files * - * @var array|null + * @var iterable|null */ protected $_configFiles = null; @@ -31,40 +42,25 @@ class Factory private $isDefaultTranslatorInitialized = false; /** - * @var \Magento\Framework\Module\Dir\Reader + * @var Reader */ private $moduleReader; - /** - * @var FrontendInterface - */ - private $cache; - - /** - * @var \Magento\Framework\Serialize\SerializerInterface - */ - private $serializer; - - /** - * @var \Magento\Framework\Config\FileIteratorFactory - */ - private $fileIteratorFactory; - /** * Initialize dependencies * - * @param \Magento\Framework\ObjectManagerInterface $objectManager - * @param \Magento\Framework\Module\Dir\Reader $moduleReader - * @param FrontendInterface $cache + * @param ObjectManagerInterface $objectManager + * @param Reader $moduleReader + * @param FrontendInterface $cache @deprecated + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - \Magento\Framework\ObjectManagerInterface $objectManager, - \Magento\Framework\Module\Dir\Reader $moduleReader, + ObjectManagerInterface $objectManager, + Reader $moduleReader, FrontendInterface $cache ) { $this->_objectManager = $objectManager; $this->moduleReader = $moduleReader; - $this->cache = $cache; } /** @@ -75,17 +71,7 @@ public function __construct( protected function _initializeConfigList() { if (!$this->_configFiles) { - $this->_configFiles = $this->cache->load(self::CACHE_KEY); - if (!$this->_configFiles) { - $this->_configFiles = $this->moduleReader->getConfigurationFiles('validation.xml'); - $this->cache->save( - $this->getSerializer()->serialize($this->_configFiles->toArray()), - self::CACHE_KEY - ); - } else { - $filesArray = $this->getSerializer()->unserialize($this->_configFiles); - $this->_configFiles = $this->getFileIteratorFactory()->create(array_keys($filesArray)); - } + $this->_configFiles = $this->moduleReader->getConfigurationFiles('validation.xml'); } } @@ -93,6 +79,7 @@ protected function _initializeConfigList() * Create and set default translator to \Magento\Framework\Validator\AbstractValidator. * * @return void + * @throws \Zend_Translate_Exception */ protected function _initializeDefaultTranslator() { @@ -100,7 +87,7 @@ protected function _initializeDefaultTranslator() // Pass translations to \Magento\Framework\TranslateInterface from validators $translatorCallback = function () { $argc = func_get_args(); - return (string)new \Magento\Framework\Phrase(array_shift($argc), $argc); + return (string)new Phrase(array_shift($argc), $argc); }; /** @var \Magento\Framework\Translate\Adapter $translator */ $translator = $this->_objectManager->create(\Magento\Framework\Translate\Adapter::class); @@ -115,14 +102,15 @@ protected function _initializeDefaultTranslator() * * Will instantiate \Magento\Framework\Validator\Config * - * @return \Magento\Framework\Validator\Config + * @return Config + * @throws \Zend_Translate_Exception */ public function getValidatorConfig() { $this->_initializeConfigList(); $this->_initializeDefaultTranslator(); return $this->_objectManager->create( - \Magento\Framework\Validator\Config::class, + Config::class, ['configFiles' => $this->_configFiles] ); } @@ -133,7 +121,8 @@ public function getValidatorConfig() * @param string $entityName * @param string $groupName * @param array|null $builderConfig - * @return \Magento\Framework\Validator\Builder + * @return Builder + * @throws \Zend_Translate_Exception */ public function createValidatorBuilder($entityName, $groupName, array $builderConfig = null) { @@ -147,43 +136,12 @@ public function createValidatorBuilder($entityName, $groupName, array $builderCo * @param string $entityName * @param string $groupName * @param array|null $builderConfig - * @return \Magento\Framework\Validator + * @return Validator + * @throws \Zend_Translate_Exception */ public function createValidator($entityName, $groupName, array $builderConfig = null) { $this->_initializeDefaultTranslator(); return $this->getValidatorConfig()->createValidator($entityName, $groupName, $builderConfig); } - - /** - * Get serializer - * - * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 100.2.0 - */ - private function getSerializer() - { - if ($this->serializer === null) { - $this->serializer = $this->_objectManager->get( - \Magento\Framework\Serialize\SerializerInterface::class - ); - } - return $this->serializer; - } - - /** - * Get file iterator factory - * - * @return \Magento\Framework\Config\FileIteratorFactory - * @deprecated 100.2.0 - */ - private function getFileIteratorFactory() - { - if ($this->fileIteratorFactory === null) { - $this->fileIteratorFactory = $this->_objectManager->get( - \Magento\Framework\Config\FileIteratorFactory::class - ); - } - return $this->fileIteratorFactory; - } } diff --git a/lib/internal/Magento/Framework/Validator/Test/Unit/FactoryTest.php b/lib/internal/Magento/Framework/Validator/Test/Unit/FactoryTest.php index 5511627c6dcc3..73a8c95c9a2ff 100644 --- a/lib/internal/Magento/Framework/Validator/Test/Unit/FactoryTest.php +++ b/lib/internal/Magento/Framework/Validator/Test/Unit/FactoryTest.php @@ -25,21 +25,6 @@ class FactoryTest extends \PHPUnit\Framework\TestCase */ private $validatorConfigMock; - /** - * @var \Magento\Framework\Cache\FrontendInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $cacheMock; - - /** - * @var \Magento\Framework\Serialize\SerializerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $serializerMock; - - /** - * @var \Magento\Framework\Config\FileIteratorFactory|\PHPUnit_Framework_MockObject_MockObject - */ - private $fileIteratorFactoryMock; - /** * @var \Magento\Framework\Config\FileIterator|\PHPUnit_Framework_MockObject_MockObject */ @@ -55,11 +40,6 @@ class FactoryTest extends \PHPUnit\Framework\TestCase */ private $factory; - /** - * @var string - */ - private $jsonString = '["\/tmp\/moduleOne\/etc\/validation.xml"]'; - /** * @var array */ @@ -99,23 +79,9 @@ protected function setUp() \Magento\Framework\Validator\Factory::class, [ 'objectManager' => $this->objectManagerMock, - 'moduleReader' => $this->readerMock, - 'cache' => $this->cacheMock + 'moduleReader' => $this->readerMock ] ); - - $this->serializerMock = $this->createMock(\Magento\Framework\Serialize\SerializerInterface::class); - $this->fileIteratorFactoryMock = $this->createMock(\Magento\Framework\Config\FileIteratorFactory::class); - $objectManager->setBackwardCompatibleProperty( - $this->factory, - 'serializer', - $this->serializerMock - ); - $objectManager->setBackwardCompatibleProperty( - $this->factory, - 'fileIteratorFactory', - $this->fileIteratorFactoryMock - ); } /** @@ -147,46 +113,6 @@ public function testGetValidatorConfig() ); } - public function testGetValidatorConfigCacheNotExist() - { - $this->cacheMock->expects($this->once()) - ->method('load') - ->willReturn(false); - $this->readerMock->expects($this->once()) - ->method('getConfigurationFiles') - ->willReturn($this->fileIteratorMock); - $this->fileIteratorMock->method('toArray') - ->willReturn($this->data); - $this->cacheMock->expects($this->once()) - ->method('save') - ->with($this->jsonString); - $this->serializerMock->expects($this->once()) - ->method('serialize') - ->with($this->data) - ->willReturn($this->jsonString); - $this->factory->getValidatorConfig(); - $this->factory->getValidatorConfig(); - } - - public function testGetValidatorConfigCacheExist() - { - $this->cacheMock->expects($this->once()) - ->method('load') - ->willReturn($this->jsonString); - $this->readerMock->expects($this->never()) - ->method('getConfigurationFiles'); - $this->cacheMock->expects($this->never()) - ->method('save'); - $this->serializerMock->expects($this->once()) - ->method('unserialize') - ->with($this->jsonString) - ->willReturn($this->data); - $this->fileIteratorFactoryMock->method('create') - ->willReturn($this->fileIteratorMock); - $this->factory->getValidatorConfig(); - $this->factory->getValidatorConfig(); - } - public function testCreateValidatorBuilder() { $this->readerMock->method('getConfigurationFiles') diff --git a/lib/internal/Magento/Framework/View/Page/Config.php b/lib/internal/Magento/Framework/View/Page/Config.php index 226abc538112b..da7bcb128f4b8 100644 --- a/lib/internal/Magento/Framework/View/Page/Config.php +++ b/lib/internal/Magento/Framework/View/Page/Config.php @@ -498,7 +498,7 @@ public function addRss($title, $href) */ public function addBodyClass($className) { - $className = preg_replace('#[^a-z0-9]+#', '-', strtolower($className)); + $className = preg_replace('#[^a-z0-9-_]+#', '-', strtolower($className)); $bodyClasses = $this->getElementAttribute(self::ELEMENT_TYPE_BODY, self::BODY_ATTRIBUTE_CLASS); $bodyClasses = $bodyClasses ? explode(' ', $bodyClasses) : []; $bodyClasses[] = $className; diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/Generator/BodyTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/Generator/BodyTest.php index 0f59c302f943f..ed926afa00856 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/Generator/BodyTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/Generator/BodyTest.php @@ -57,13 +57,13 @@ public function testProcess() ->method('getPageConfigStructure') ->willReturn($structureMock); - $bodyClasses = ['class_1', 'class_2']; + $bodyClasses = ['class_1', 'class--2']; $structureMock->expects($this->once()) ->method('getBodyClasses') ->will($this->returnValue($bodyClasses)); $this->pageConfigMock->expects($this->exactly(2)) ->method('addBodyClass') - ->withConsecutive(['class_1'], ['class_2']); + ->withConsecutive(['class_1'], ['class--2']); $this->assertEquals( $this->bodyGenerator, diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/StructureTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/StructureTest.php index ed15a356cc4c7..d2eba5d2fa1b3 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/StructureTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/StructureTest.php @@ -58,7 +58,7 @@ public function testSetElementAttribute() public function testSetBodyClass() { $class1 = 'class_1'; - $class2 = 'class_2'; + $class2 = 'class--2'; $expected = [$class1, $class2]; $this->structure->setBodyClass($class1); $this->structure->setBodyClass($class2); diff --git a/lib/internal/Magento/Framework/composer.json b/lib/internal/Magento/Framework/composer.json index 105c3f0721819..c674010af1a1f 100644 --- a/lib/internal/Magento/Framework/composer.json +++ b/lib/internal/Magento/Framework/composer.json @@ -2,7 +2,7 @@ "name": "magento/framework", "description": "N/A", "type": "magento2-library", - "version": "101.0.7", + "version": "101.0.8", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/lib/web/css/source/lib/_icons.less b/lib/web/css/source/lib/_icons.less index d113935e2b1cd..abb8b43368f13 100644 --- a/lib/web/css/source/lib/_icons.less +++ b/lib/web/css/source/lib/_icons.less @@ -25,9 +25,12 @@ @_icon-font-text-hide: @icon-font__text-hide, @_icon-font-display: @icon-font__display ) when (@_icon-font-position = before) { - ._lib-icon-text-hide(@_icon-font-text-hide); .lib-css(display, @_icon-font-display); - text-decoration: none; + text-decoration: none; + + & when not (@_icon-font-content = false) { + ._lib-icon-text-hide(@_icon-font-text-hide); + } &:before { ._lib-icon-font( @@ -68,10 +71,13 @@ @_icon-font-text-hide: @icon-font__text-hide, @_icon-font-display: @icon-font__display ) when (@_icon-font-position = after) { - ._lib-icon-text-hide(@_icon-font-text-hide); .lib-css(display, @_icon-font-display); text-decoration: none; - + + & when not (@_icon-font-content = false) { + ._lib-icon-text-hide(@_icon-font-text-hide); + } + &:after { ._lib-icon-font( @_icon-font-content, @@ -151,8 +157,11 @@ @_icon-image-text-hide: @icon__text-hide ) when (@_icon-image-position = before) { display: inline-block; - ._lib-icon-text-hide(@_icon-image-text-hide); - + + & when not (@_icon-image = false) { + ._lib-icon-text-hide(@_icon-image-text-hide); + } + &:before { ._lib-icon-image( @_icon-image, @@ -179,7 +188,10 @@ @_icon-image-text-hide: @icon__text-hide ) when (@_icon-image-position = after) { display: inline-block; - ._lib-icon-text-hide(@_icon-image-text-hide); + + & when not (@_icon-image = false) { + ._lib-icon-text-hide(@_icon-font-text-hide); + } &:after { ._lib-icon-image( diff --git a/lib/web/fotorama/fotorama.js b/lib/web/fotorama/fotorama.js index 4f323e4312f6b..33d731937707c 100644 --- a/lib/web/fotorama/fotorama.js +++ b/lib/web/fotorama/fotorama.js @@ -1455,16 +1455,24 @@ fotoramaVersion = '4.6.4'; } } else { stopEvent(e); - (options.onMove || noop).call(el, e, {touch: touchFLAG}); + if (movedEnough(xDiff,yDiff)) { + (options.onMove || noop).call(el, e, {touch: touchFLAG}); + } } - if (!moved && Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2)) > tolerance) { + if (!moved && movedEnough(xDiff, yDiff) && Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2)) > tolerance) + { moved = true; } tail.checked = tail.checked || xWin || yWin; } + + function movedEnough(xDiff, yDiff) { + return xDiff > yDiff && xDiff > 1.5; + } + function onEnd(e) { (options.onTouchEnd || noop)(); diff --git a/lib/web/fotorama/fotorama.min.js b/lib/web/fotorama/fotorama.min.js index e8eb9fbda63ef..f416d57488925 100644 --- a/lib/web/fotorama/fotorama.min.js +++ b/lib/web/fotorama/fotorama.min.js @@ -1,4 +1 @@ -/*! - * Fotorama 4.6.4 | http://fotorama.io/license/ - */ -fotoramaVersion="4.6.4";(function(bo,k,a3,bV,aP){var ag="fotorama",bH="fotorama__fullscreen",ae=ag+"__wrap",ah=ae+"--css2",aX=ae+"--css3",bt=ae+"--video",ar=ae+"--fade",aw=ae+"--slide",P=ae+"--no-controls",aM=ae+"--no-shadows",U=ae+"--pan-y",a0=ae+"--rtl",az=ae+"--only-active",bN=ae+"--no-captions",f=ae+"--toggle-arrows",a7=ag+"__stage",x=a7+"__frame",l=x+"--video",B=a7+"__shaft",aB=ag+"__grab",bC=ag+"__pointer",aK=ag+"__arr",F=aK+"--disabled",bc=aK+"--prev",r=aK+"--next",bO=ag+"__nav",bq=bO+"-wrap",aH=bO+"__shaft",b=bq+"--vertical",ax=bq+"--list",bZ=bq+"--horizontal",bW=bO+"--dots",ai=bO+"--thumbs",aG=bO+"__frame",br=ag+"__fade",al=br+"-front",n=br+"-rear",aW=ag+"__shadow",bz=aW+"s",S=bz+"--left",aL=bz+"--right",a2=bz+"--top",aR=bz+"--bottom",a4=ag+"__active",a9=ag+"__select",bs=ag+"--hidden",M=ag+"--fullscreen",aJ=ag+"__fullscreen-icon",bP=ag+"__error",bM=ag+"__loading",c=ag+"__loaded",b3=c+"--full",bg=c+"--img",bR=ag+"__grabbing",J=ag+"__img",Y=J+"--full",bS=ag+"__thumb",b0=bS+"__arr--left",H=bS+"__arr--right",cb=bS+"-border",bd=ag+"__html",af=ag+"-video-container",bJ=ag+"__video",T=bJ+"-play",w=bJ+"-close",au=ag+"_horizontal_ratio",aY=ag+"_vertical_ratio",ca=ag+"__spinner",Z=ca+"--show";var E=bV&&bV.fn.jquery.split(".");if(!E||E[0]<1||(E[0]==1&&E[1]<8)){throw"Fotorama requires jQuery 1.8 or later and will not run without it."}var bx={};var ap=(function(co,ct,cj){var cf="2.8.3",cm={},cD=ct.documentElement,cE="modernizr",cB=ct.createElement(cE),cp=cB.style,cg,cw={}.toString,cy=" -webkit- -moz- -o- -ms- ".split(" "),cd="Webkit Moz O ms",cG=cd.split(" "),cq=cd.toLowerCase().split(" "),ck={},ce={},cu={},cA=[],cv=cA.slice,cc,cz=function(cQ,cS,cK,cR){var cJ,cP,cM,cN,cI=ct.createElement("div"),cO=ct.body,cL=cO||ct.createElement("body");if(parseInt(cK,10)){while(cK--){cM=ct.createElement("div");cM.id=cR?cR[cK]:cE+(cK+1);cI.appendChild(cM)}}cJ=["­",'"].join("");cI.id=cE;(cO?cI:cL).innerHTML+=cJ;cL.appendChild(cI);if(!cO){cL.style.background="";cL.style.overflow="hidden";cN=cD.style.overflow;cD.style.overflow="hidden";cD.appendChild(cL)}cP=cS(cI,cQ);if(!cO){cL.parentNode.removeChild(cL);cD.style.overflow=cN}else{cI.parentNode.removeChild(cI)}return !!cP},cs=({}).hasOwnProperty,cC;if(!cl(cs,"undefined")&&!cl(cs.call,"undefined")){cC=function(cI,cJ){return cs.call(cI,cJ)}}else{cC=function(cI,cJ){return((cJ in cI)&&cl(cI.constructor.prototype[cJ],"undefined"))}}if(!Function.prototype.bind){Function.prototype.bind=function cH(cK){var cL=this;if(typeof cL!="function"){throw new TypeError()}var cI=cv.call(arguments,1),cJ=function(){if(this instanceof cJ){var cO=function(){};cO.prototype=cL.prototype;var cN=new cO();var cM=cL.apply(cN,cI.concat(cv.call(arguments)));if(Object(cM)===cM){return cM}return cN}else{return cL.apply(cK,cI.concat(cv.call(arguments)))}};return cJ}}function cr(cI){cp.cssText=cI}function ci(cJ,cI){return cr(cy.join(cJ+";")+(cI||""))}function cl(cJ,cI){return typeof cJ===cI}function cn(cJ,cI){return !!~(""+cJ).indexOf(cI)}function cF(cK,cI){for(var cJ in cK){var cL=cK[cJ];if(!cn(cL,"-")&&cp[cL]!==cj){return cI=="pfx"?cL:true}}return false}function cx(cJ,cM,cL){for(var cI in cJ){var cK=cM[cJ[cI]];if(cK!==cj){if(cL===false){return cJ[cI]}if(cl(cK,"function")){return cK.bind(cL||cM)}return cK}}return false}function i(cM,cI,cL){var cJ=cM.charAt(0).toUpperCase()+cM.slice(1),cK=(cM+" "+cG.join(cJ+" ")+cJ).split(" ");if(cl(cI,"string")||cl(cI,"undefined")){return cF(cK,cI)}else{cK=(cM+" "+(cq).join(cJ+" ")+cJ).split(" ");return cx(cK,cI,cL)}}ck.touch=function(){var cI;if(("ontouchstart" in co)||co.DocumentTouch&&ct instanceof DocumentTouch){cI=true}else{cz(["@media (",cy.join("touch-enabled),("),cE,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(cJ){cI=cJ.offsetTop===9})}return cI};ck.csstransforms3d=function(){var cI=!!i("perspective");if(cI&&"webkitPerspective" in cD.style){cz("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(cJ,cK){cI=cJ.offsetLeft===9&&cJ.offsetHeight===3})}return cI};ck.csstransitions=function(){return i("transition")};for(var ch in ck){if(cC(ck,ch)){cc=ch.toLowerCase();cm[cc]=ck[ch]();cA.push((cm[cc]?"":"no-")+cc)}}cm.addTest=function(cJ,cK){if(typeof cJ=="object"){for(var cI in cJ){if(cC(cJ,cI)){cm.addTest(cI,cJ[cI])}}}else{cJ=cJ.toLowerCase();if(cm[cJ]!==cj){return cm}cK=typeof cK=="function"?cK():cK;if(typeof enableClasses!=="undefined"&&enableClasses){cD.className+=" "+(cK?"":"no-")+cJ}cm[cJ]=cK}return cm};cr("");cB=cg=null;cm._version=cf;cm._prefixes=cy;cm._domPrefixes=cq;cm._cssomPrefixes=cG;cm.testProp=function(cI){return cF([cI])};cm.testAllProps=i;cm.testStyles=cz;cm.prefixed=function(cK,cJ,cI){if(!cJ){return i(cK,"pfx")}else{return i(cK,cJ,cI)}};return cm})(bo,k);var bB={ok:false,is:function(){return false},request:function(){},cancel:function(){},event:"",prefix:""},h="webkit moz o ms khtml".split(" ");if(typeof k.cancelFullScreen!="undefined"){bB.ok=true}else{for(var bv=0,N=h.length;bv=i?"bottom":"top bottom"):(ce<=cd?"left":ce>=i?"right":"left right")}function z(cc,cd,i){i=i||{};cc.each(function(){var cg=bV(this),cf=cg.data(),ce;if(cf.clickOn){return}cf.clickOn=true;bV.extend(aI(cg,{onStart:function(ch){ce=ch;(i.onStart||g).call(this,ch)},onMove:i.onMove||g,onTouchEnd:i.onTouchEnd||g,onEnd:function(ch){if(ch.moved){return}cd.call(this,ce)}}),{noMove:true})})}function ab(i,cc){return'
'+(cc||"")+"
"}function aT(i){return"."+i}function q(i){var cc='';return cc}function aC(cf){var cc=cf.length;while(cc){var ce=Math.floor(Math.random()*cc--);var cd=cf[cc];cf[cc]=cf[ce];cf[ce]=cd}return cf}function bG(i){return Object.prototype.toString.call(i)=="[object Array]"&&bV.map(i,function(cc){return bV.extend({},cc)})}function bU(i,cd,cc){i.scrollLeft(cd||0).scrollTop(cc||0)}function bA(i){if(i){var cc={};bV.each(i,function(cd,ce){cc[cd.toLowerCase()]=ce});return cc}}function bm(i){if(!i){return}var cc=+i;if(!isNaN(cc)){return cc}else{cc=i.split("/");return +cc[0]/+cc[1]||aP}}function D(cd,ce,cc,i){if(!ce){return}cd.addEventListener?cd.addEventListener(ce,cc,!!i):cd.attachEvent("on"+ce,cc)}function a5(i,cc){if(i>cc.max){i=cc.max}else{if(i=(ci-cf)){if(cc==="horizontal"){cg=-ce.position().left}else{cg=-ce.position().top}}else{if((cj+i.margin)*(ch)<=Math.abs(cf)){if(cc==="horizontal"){cg=-ce.position().left+ci-(cj+i.margin)}else{cg=-ce.position().top+ci-(cj+i.margin)}}else{cg=cf}}cg=a5(cg,ck);return cg||0}function aj(i){return !!i.getAttribute("disabled")}function ad(cc,i){if(i){return{disabled:cc}}else{return{tabindex:cc*-1+"",disabled:cc}}}function a(cc,i){D(cc,"keyup",function(cd){aj(cc)||cd.keyCode==13&&i.call(cc,cd)})}function bL(cc,i){D(cc,"focus",cc.onfocusin=function(cd){i.call(cc,cd)},true)}function O(cc,i){cc.preventDefault?cc.preventDefault():(cc.returnValue=false);i&&cc.stopPropagation&&cc.stopPropagation()}function aE(cd,cc){var i=/iP(ad|hone|od)/i.test(bo.navigator.userAgent);if(i&&cc==="touchend"){cd.on("touchend",function(ce){bw.trigger("mouseup",ce)})}cd.on(cc,function(ce){O(ce,true);return false})}function ay(i){return i?">":"<"}var aS=(function(){function cd(ch,ce,cg){var cf=ce/cg;if(cf<=1){ch.parent().removeClass(au);ch.parent().addClass(aY)}else{ch.parent().removeClass(aY);ch.parent().addClass(au)}}function i(cf,cg,ch){var ce=ch;if(!cf.attr(ce)&&cf.attr(ce)!==aP){cf.attr(ce,cg)}if(cf.find("["+ce+"]").length){cf.find("["+ce+"]").each(function(){bV(this).attr(ce,cg)})}}function cc(cf,ce,ci){var cg=false,ch;cf.showCaption===ci||cf.showCaption===true?ch=true:ch=false;if(!ce){return false}if(cf.caption&&ch){cg=true}return cg}return{setRatio:cd,setThumbAttr:i,isExpectedCaption:cc}}(aS||{},jQuery));function A(ce,cd){var cc=ce.data(),i=Math.round(cd.pos),cf=function(){if(cc&&cc.sliding){cc.sliding=false}(cd.onEnd||g)()};if(typeof cd.overPos!=="undefined"&&cd.overPos!==cd.pos){i=cd.overPos}var cg=bV.extend(b2(i,cd.direction),cd.width&&{width:cd.width},cd.height&&{height:cd.height});if(cc&&cc.sliding){cc.sliding=true}if(aA){ce.css(bV.extend(b6(cd.time),cg));if(cd.time>10){X(ce,"transform",cf,cd.time)}else{cf()}}else{ce.stop().animate(cg,cd.time,u,cf)}}function aq(ck,cj,cc,cm,ce,i){var ch=typeof i!=="undefined";if(!ch){ce.push(arguments);Array.prototype.push.call(arguments,ce.length);if(ce.length>1){return}}ck=ck||bV(ck);cj=cj||bV(cj);var ci=ck[0],cg=cj[0],cf=cm.method==="crossfade",cl=function(){if(!cl.done){cl.done=true;var cn=(ch||ce.shift())&&ce.shift();cn&&aq.apply(this,cn);(cm.onEnd||g)(!!cn)}},cd=cm.time/(i||1);cc.removeClass(n+" "+al);ck.stop().addClass(n);cj.stop().addClass(al);cf&&cg&&ck.fadeTo(0,0);ck.fadeTo(cf?cd:0,1,cf&&cl);cj.fadeTo(cd,0,cl);(ci&&cf)||cg||cl()}var G,b5,e,j,bD;function bn(i){var cc=(i.touches||[])[0]||i;i._x=cc.pageX||cc.originalEvent.pageX;i._y=cc.clientY||cc.originalEvent.clientY;i._now=bV.now()}function aI(cr,cg){var cc=cr[0],cj={},i,cl,cf,cn,cs,cd,ce,co,ch;function cq(ct){cf=bV(ct.target);cj.checked=cd=ce=ch=false;if(i||cj.flow||(ct.touches&&ct.touches.length>1)||ct.which>1||(G&&G.type!==ct.type&&e)||(cd=cg.select&&cf.is(cg.select,cc))){return cd}cs=ct.type==="touchstart";ce=cf.is("a, a *",cc);cn=cj.control;co=(cj.noMove||cj.noSwipe||cn)?16:!cj.snap?4:0;bn(ct);cl=G=ct;b5=ct.type.replace(/down|start/,"move").replace(/Down/,"Move");(cg.onStart||g).call(cc,ct,{control:cn,$target:cf});i=cj.flow=true;if(!cs||cj.go){O(ct)}}function ck(cx){if((cx.touches&&cx.touches.length>1)||(aZ&&!cx.isPrimary)||b5!==cx.type||!i){i&&ci();(cg.onTouchEnd||g)();return}bn(cx);var cy=Math.abs(cx._x-cl._x),cu=Math.abs(cx._y-cl._y),cw=cy-cu,cv=(cj.go||cj.x||cw>=0)&&!cj.noSwipe,ct=cw<0;if(cs&&!cj.checked){if(i=cv){O(cx)}}else{O(cx);(cg.onMove||g).call(cc,cx,{touch:cs})}if(!ch&&Math.sqrt(Math.pow(cy,2)+Math.pow(cu,2))>co){ch=true}cj.checked=cj.checked||cv||ct}function ci(cu){(cg.onTouchEnd||g)();var ct=i;cj.control=i=false;if(ct){cj.flow=false}if(!ct||(ce&&!cj.checked)){return}cu&&O(cu);e=true;clearTimeout(j);j=setTimeout(function(){e=false},1000);(cg.onEnd||g).call(cc,{moved:ch,$target:cf,control:cn,touch:cs,startEvent:cl,aborted:!cu||cu.type==="MSPointerCancel"})}function cm(){if(cj.flow){return}cj.flow=true}function cp(){if(!cj.flow){return}cj.flow=false}if(aZ){D(cc,"MSPointerDown",cq);D(k,"MSPointerMove",ck);D(k,"MSPointerCancel",ci);D(k,"MSPointerUp",ci)}else{D(cc,"touchstart",cq);D(cc,"touchmove",ck);D(cc,"touchend",ci);D(k,"touchstart",cm);D(k,"touchend",cp);D(k,"touchcancel",cp);bf.on("scroll",cp);cr.on("mousedown pointerdown",cq);bw.on("mousemove pointermove",ck).on("mouseup pointerup",ci)}if(ap.touch){bD="a"}else{bD="div"}cr.on("click",bD,function(ct){cj.checked&&O(ct)});return cj}function ao(cz,cd){var cc=cz[0],ce=cz.data(),cm={},cw,cf,cx,cj,ch,cy,co,cg,cr,ct,cp,cq,i,cv,ci,cn;function cs(cA,cB){cn=true;cw=cf=(cq==="vertical")?cA._y:cA._x;co=cA._now;cy=[[co,cw]];cx=cj=cm.noMove||cB?0:a1(cz,(cd.getPos||g)());(cd.onStart||g).call(cc,cA)}function cu(cB,cA){cr=cm.min;ct=cm.max;cp=cm.snap,cq=cm.direction||"horizontal",cz.navdir=cq;i=cB.altKey;cn=ci=false;cv=cA.control;if(!cv&&!ce.sliding){cs(cB)}}function cl(cB,cA){if(!cm.noSwipe){if(!cn){cs(cB)}cf=(cq==="vertical")?cB._y:cB._x;cy.push([cB._now,cf]);cj=cx-(cw-cf);ch=bp(cj,cr,ct,cq);if(cj<=cr){cj=aF(cj,cr)}else{if(cj>=ct){cj=aF(cj,ct)}}if(!cm.noMove){cz.css(b2(cj,cq));if(!ci){ci=true;cA.touch||aZ||cz.addClass(bR)}(cd.onMove||g).call(cc,cB,{pos:cj,edge:ch})}}}function ck(cJ){if(cm.noSwipe&&cJ.moved){return}if(!cn){cs(cJ.startEvent,true)}cJ.touch||aZ||cz.removeClass(bR);cg=bV.now();var cG=cg-b8,cK,cP,cQ,cS=null,cA,cE,cN,cD,cF,cI=ba,cO,cH=cd.friction;for(var cC=cy.length-1;cC>=0;cC--){cK=cy[cC][0];cP=Math.abs(cK-cG);if(cS===null||cPcQ){break}}cQ=cP}cD=bb(cj,cr,ct);var cT=cA-cf,cR=cT>=0,cL=cg-cS,cB=cL>b8,cM=!cB&&cj!==cx&&cD===cj;if(cp){cD=bb(Math[cM?(cR?"floor":"ceil"):"round"](cj/cp)*cp,cr,ct);cr=ct=cD}if(cM&&(cp||cD===cj)){cO=-(cT/cL);cI*=bb(Math.abs(cO),cd.timeLow,cd.timeHigh);cE=Math.round(cj+cO*cI/cH);if(!cp){cD=cE}if(!cR&&cE>ct||cR&&cE"),c9=bV(ab(bs)),dk=d6.find(aT(ae)),cf=dk.find(aT(a7)),dY=cf[0],cl=d6.find(aT(B)),c8=bV(),dW=d6.find(aT(bc)),da=d6.find(aT(r)),cU=d6.find(aT(aK)),dU=d6.find(aT(bq)),dO=dU.find(aT(bO)),cF=dO.find(aT(aH)),dA,cB=bV(),cW=bV(),dS=cl.data(),cX=cF.data(),c7=d6.find(aT(cb)),eg=d6.find(aT(b0)),dX=d6.find(aT(H)),dM=d6.find(aT(aJ)),dD=dM[0],cH=bV(ab(T)),dt=d6.find(aT(w)),d1=dt[0],eb=d6.find(aT(ca)),dg,eo=false,dF,ea,c2,ed,dw,d4,cN,cK,dx,dj,cq,c0,d8,c4,d2,cv,ch,ej,ds,cu,ec,dH,dE,d0={},en={},dG,d5={},cG={},dy={},ef={},cs,cT,ee,cj,el,cd={},er={},dZ,c6,dz,dr,d3=0,cI=[];dk[bu]=bV('
');dk[bl]=bV(bV.Fotorama.jst.thumb());dk[b7]=bV(bV.Fotorama.jst.dots());cd[bu]=[];cd[bl]=[];cd[b7]=[];er[bu]={};dk.addClass(aA?aX:ah);cR.fotorama=this;function ep(){bV.each(dP,function(ey,eA){if(!eA.i){eA.i=cY++;var ez=at(eA.video,true);if(ez){var ex={};eA.video=ez;if(!eA.img&&!eA.thumb){ex=aQ(eA,dP,cg)}else{eA.thumbsReady=true}v(dP,{img:ex.img,thumb:ex.thumb},eA.i,cg)}}})}function df(ex){return dE[ex]}function i(){if(cf!==aP){if(c3.navdir=="vertical"){var ex=c3.thumbwidth+c3.thumbmargin;cf.css("left",ex);da.css("right",ex);dM.css("right",ex);dk.css("width",dk.css("width")+ex);cl.css("max-width",dk.width()-ex)}else{cf.css("left","");da.css("right","");dM.css("right","");dk.css("width",dk.css("width")+ex);cl.css("max-width","")}}}function ek(eB){var eC="keydown."+ag,eD=ag+cC,ex="keydown."+eD,eA="keyup."+eD,ey="resize."+eD+" orientationchange."+eD,ez;if(eB){bw.on(ex,function(eG){var eF,eE;if(dg&&eG.keyCode===27){eF=true;cO(dg,true,true)}else{if(cg.fullScreen||(c3.keyboard&&!cg.index)){if(eG.keyCode===27){eF=true;cg.cancelFullScreen()}else{if((eG.shiftKey&&eG.keyCode===32&&df("space"))||(!eG.altKey&&!eG.metaKey&&eG.keyCode===37&&df("left"))||(eG.keyCode===38&&df("up")&&bV(":focus").attr("data-gallery-role"))){cg.longPress.progress();eE="<"}else{if((eG.keyCode===32&&df("space"))||(!eG.altKey&&!eG.metaKey&&eG.keyCode===39&&df("right"))||(eG.keyCode===40&&df("down")&&bV(":focus").attr("data-gallery-role"))){cg.longPress.progress();eE=">"}else{if(eG.keyCode===36&&df("home")){cg.longPress.progress();eE="<<"}else{if(eG.keyCode===35&&df("end")){cg.longPress.progress();eE=">>"}}}}}}}(eF||eE)&&O(eG);ez={index:eE,slow:eG.altKey,user:true};eE&&(cg.longPress.inProgress?cg.showWhileLongPress(ez):cg.show(ez))});if(eB){bw.on(eA,function(eE){if(cg.longPress.inProgress){cg.showEndLongPress({user:true})}cg.longPress.reset()})}if(!cg.index){bw.off(eC).on(eC,"textarea, input, select",function(eE){!I.hasClass(bH)&&eE.stopPropagation()})}bf.on(ey,cg.resize)}else{bw.off(ex);bf.off(ey)}}function dd(ex){if(ex===dd.f){return}if(ex){d6.addClass(ag+" "+cQ).before(c9).before(de);C(cg)}else{c9.detach();de.detach();d6.html(cR.urtext).removeClass(cQ);av(cg)}ek(ex);dd.f=ex}function dn(){dP=cg.data=dP||bG(c3.data)||bI(d6);c1=cg.size=dP.length;eq.ok&&c3.shuffle&&aC(dP);ep();eo=cn(eo);c1&&dd(true)}function em(){var ex=c1<2||dg;d5.noMove=ex||cv;d5.noSwipe=ex||!c3.swipe;!cu&&cl.toggleClass(aB,!c3.click&&!d5.noMove&&!d5.noSwipe);aZ&&dk.toggleClass(U,!d5.noSwipe)}function dq(ex){if(ex===true){ex=""}c3.autoplay=Math.max(+ex||bQ,ds*1.5)}function db(ex){if(ex.navarrows&&ex.nav==="thumbs"){eg.show();dX.show()}else{eg.hide();dX.hide()}}function ck(ex,ey){return Math.floor(dk.width()/(ey.thumbwidth+ey.thumbmargin))}function dQ(){if(!c3.nav||c3.nav==="dots"){c3.navdir="horizontal"}cg.options=c3=bA(c3);b4=ck(dk,c3);cv=(c3.transition==="crossfade"||c3.transition==="dissolve");dj=c3.loop&&(c1>2||(cv&&(!cu||cu!=="slide")));ds=+c3.transitionduration||ba;dH=c3.direction==="rtl";dE=bV.extend({},c3.keyboard&&p,c3.keyboard);db(c3);var ey={add:[],remove:[]};function ex(ez,eA){ey[ez?"add":"remove"].push(eA)}if(c1>1){cq=c3.nav;d8=c3.navposition==="top";ey.remove.push(a9);cU.toggle(!!c3.arrows)}else{cq=false;cU.hide()}dh();cJ();ev();if(c3.autoplay){dq(c3.autoplay)}ch=m(c3.thumbwidth)||L;ej=m(c3.thumbheight)||L;cG.ok=ef.ok=c3.trackpad&&!bh;em();dL(c3,[en]);c0=cq==="thumbs";if(dU.filter(":hidden")&&!!cq){dU.show()}if(c0){dl(c1,"navThumb");dA=cW;dr=bl;an(de,bV.Fotorama.jst.style({w:ch,h:ej,b:c3.thumbborderwidth,m:c3.thumbmargin,s:cC,q:!aN}));dO.addClass(ai).removeClass(bW)}else{if(cq==="dots"){dl(c1,"navDot");dA=cB;dr=b7;dO.addClass(bW).removeClass(ai)}else{dU.hide();cq=false;dO.removeClass(ai+" "+bW)}}if(cq){if(d8){dU.insertBefore(cf)}else{dU.insertAfter(cf)}cz.nav=false;cz(dA,cF,"nav")}c4=c3.allowfullscreen;if(c4){dM.prependTo(cf);d2=s&&c4==="native";aE(dM,"touchend")}else{dM.detach();d2=false}ex(cv,ar);ex(!cv,aw);ex(!c3.captions,bN);ex(dH,a0);ex(c3.arrows,f);ec=c3.shadows&&!bh;ex(!ec,aM);dk.addClass(ey.add.join(" ")).removeClass(ey.remove.join(" "));d0=bV.extend({},c3);i()}function cZ(ex){return ex<0?(c1+(ex%c1))%c1:ex>=c1?ex%c1:ex}function cn(ex){return bb(ex,0,c1-1)}function du(ex){return dj?cZ(ex):cn(ex)}function dB(ex){return ex>0||dj?ex-1:false}function ci(ex){return ex1&&dP[eE]===eD&&!eD.html&&!eD.deleted&&!eD.video&&!eN){eD.deleted=true;cg.splice(eE,1)}}}function eL(){bV.Fotorama.measures[eF]=eO.measures=bV.Fotorama.measures[eF]||{width:eS.width,height:eS.height,ratio:eS.width/eS.height};cc(eO.measures.width,eO.measures.height,eO.measures.ratio,eE);eG.off("load error").addClass(""+(eN?Y:J)).attr("aria-hidden","false").prependTo(eC);if(eC.hasClass(x)&&!eC.hasClass(af)){eC.attr("href",eG.attr("src"))}V(eG,(bV.isFunction(eA)?eA():eA)||en);bV.Fotorama.cache[eF]=eB.state="loaded";setTimeout(function(){eC.trigger("f:load").removeClass(bM+" "+bP).addClass(c+" "+(eN?b3:bg));if(ey==="stage"){eH("load")}else{if(eD.thumbratio===bF||!eD.thumbratio&&c3.thumbratio===bF){eD.thumbratio=eO.measures.ratio;dV()}}},0)}if(!eF){eK();return}function eI(){var eT=10;bX(function(){return !c6||!eT--&&!bh},function(){eL()})}if(!bV.Fotorama.cache[eF]){bV.Fotorama.cache[eF]="*";eG.on("load",eI).on("error",eK)}else{(function eQ(){if(bV.Fotorama.cache[eF]==="error"){eK()}else{if(bV.Fotorama.cache[eF]==="loaded"){setTimeout(eI,0)}else{setTimeout(eQ,100)}}})()}eB.state="";eS.src=eF;if(eB.data.caption){eS.alt=eB.data.caption||""}if(eB.data.full){bV(eS).data("original",eB.data.full)}if(aS.isExpectedCaption(eD,c3.showcaption)){bV(eS).attr("aria-labelledby",eD.labelledby)}})}function cy(){var ex=dF[bu];if(ex&&!ex.data().state){eb.addClass(Z);ex.on("f:load f:error",function(){ex.off("f:load f:error");eb.removeClass(Z)})}}function cL(ex){a(ex,dJ);bL(ex,function(){setTimeout(function(){bU(dO)},0);dT({time:ds,guessIndex:bV(this).data().eq,minMax:dy})})}function dl(ex,ey){dm(ex,ey,function(eB,ez,eG,eD,eA,eC){if(eD){return}eD=eG[eA]=dk[eA].clone();eC=eD.data();eC.data=eG;var eF=eD[0],eE="labelledby"+bV.now();if(ey==="stage"){if(eG.html){bV('
').append(eG._html?bV(eG.html).removeAttr("id").html(eG._html):eG.html).appendTo(eD)}if(eG.id){eE=eG.id||eE}eG.labelledby=eE;if(aS.isExpectedCaption(eG,c3.showcaption)){bV(bV.Fotorama.jst.frameCaption({caption:eG.caption,labelledby:eE})).appendTo(eD)}eG.video&&eD.addClass(l).append(cH.clone());bL(eF,function(){setTimeout(function(){bU(cf)},0);cm({index:eC.eq,user:true})});c8=c8.add(eD)}else{if(ey==="navDot"){cL(eF);cB=cB.add(eD)}else{if(ey==="navThumb"){cL(eF);eC.$wrap=eD.children(":first");cW=cW.add(eD);if(eG.video){eC.$wrap.append(cH.clone())}}}}})}function cM(ey,ex){return ey&&ey.length&&V(ey,ex)}function di(ex){dm(ex,"stage",function(eB,ez,eE,eD,eA,eC){if(!eD){return}var ey=cZ(ez);eC.eq=ey;er[bu][ey]=eD.css(bV.extend({left:cv?0:a8(ez,en.w,c3.margin,c2)},cv&&b6(0)));if(be(eD[0])){eD.appendTo(cl);cO(eE.$video)}cM(eC.$img,en);cM(eC.$full,en);if(eD.hasClass(x)&&!(eD.attr("aria-hidden")==="false"&&eD.hasClass(a4))){eD.attr("aria-hidden","true")}})}function dp(eB,ex){var ey,ez,eA;if(cq!=="thumbs"||isNaN(eB)){return}ey=-eB;ez=-eB+en.nw;if(c3.navdir==="vertical"){eB=eB-c3.thumbheight;ez=-eB+en.h}cW.each(function(){var eH=bV(this),eD=eH.data(),eC=eD.eq,eG=function(){return{h:ej,w:eD.w}},eF=eG(),eE=c3.navdir==="vertical"?eD.t>ez:eD.l>ez;eF.w=eD.w;if(eD.l+eD.wen.w/3}function cE(ex){return !dj&&(!(eo+ex)||!(eo-c1+ex))&&!dg}function dh(){var ey=cE(0),ex=cE(1);dW.toggleClass(F,ey).attr(ad(ey,false));da.toggleClass(F,ex).attr(ad(ex,false))}function ev(){var ex=false,ey=false;if(c3.navtype==="thumbs"&&!c3.loop){(eo==0)?ex=true:ex=false;(eo==c3.data.length-1)?ey=true:ey=false}if(c3.navtype==="slides"){var ez=aa(cF,c3.navdir);ez>=dy.max?ex=true:ex=false;ez<=dy.min?ey=true:ey=false}eg.toggleClass(F,ex).attr(ad(ex,true));dX.toggleClass(F,ey).attr(ad(ey,true))}function cJ(){if(cG.ok){cG.prevent={"<":cE(0),">":cE(1)}}}function dI(eD){var eA=eD.data(),eC,eB,ez,ex;if(c0){eC=eA.l;eB=eA.t;ez=eA.w;ex=eA.h}else{eC=eD.position().left;ez=eD.width()}var ey={c:eC+ez/2,min:-eC+c3.thumbmargin*10,max:-eC+en.w-ez-c3.thumbmargin*10};var eE={c:eB+ex/2,min:-eB+c3.thumbmargin*10,max:-eB+en.h-ex-c3.thumbmargin*10};return c3.navdir==="vertical"?eE:ey}function d7(ey){var ex=dF[dr].data();A(c7,{time:ey*1.2,pos:(c3.navdir==="vertical"?ex.t:ex.l),width:ex.w,height:ex.h,direction:c3.navdir})}function dT(eH){var eB=dP[eH.guessIndex][dr],ez=c3.navtype;var eD,ex,eA,eG,eC,ey,eE,eF;if(eB){if(ez==="thumbs"){eD=dy.min!==dy.max;eA=eH.minMax||eD&&dI(dF[dr]);eG=eD&&(eH.keep&&dT.t?dT.l:bb((eH.coo||en.nw/2)-dI(eB).c,eA.min,eA.max));eC=eD&&(eH.keep&&dT.l?dT.l:bb((eH.coo||en.nw/2)-dI(eB).c,eA.min,eA.max));ey=(c3.navdir==="vertical"?eG:eC);eE=eD&&bb(ey,dy.min,dy.max)||0;ex=eH.time*1.1;A(cF,{time:ex,pos:eE,direction:c3.navdir,onEnd:function(){dp(eE,true);ev()}});co(dO,bp(eE,dy.min,dy.max,c3.navdir));dT.l=ey}else{eF=aa(cF,c3.navdir);ex=eH.time*1.11;eE=aD(c3,dy,eH.guessIndex,eF,eB,dU,c3.navdir);A(cF,{time:ex,pos:eE,direction:c3.navdir,onEnd:function(){dp(eE,true);ev()}});co(dO,bp(eE,dy.min,dy.max,c3.navdir))}}}function cS(){dN(dr);cd[dr].push(dF[dr].addClass(a4).attr("data-active",true))}function dN(ey){var ex=cd[ey];while(ex.length){ex.shift().removeClass(a4).attr("data-active",false)}}function ce(ey){var ex=er[ey];bV.each(ea,function(eA,ez){delete ex[cZ(ez)]});bV.each(ex,function(ez,eA){delete ex[ez];eA.detach()})}function dC(ey){c2=ed=eo;var ex=dF[bu];if(ex){dN(bu);cd[bu].push(ex.addClass(a4).attr("data-active",true));if(ex.hasClass(x)){ex.attr("aria-hidden","false")}ey||cg.showStage.onEnd(true);a1(cl,0,true);ce(bu);di(ea);d9();c5();a(cl[0],function(){if(!d6.hasClass(M)){cg.requestFullScreen();dM.focus()}})}}function dL(ey,ex){if(!ey){return}bV.each(ex,function(ez,eA){if(!eA){return}bV.extend(eA,{width:ey.width||eA.width,height:ey.height,minwidth:ey.minwidth,maxwidth:ey.maxwidth,minheight:ey.minheight,maxheight:ey.maxheight,ratio:bm(ey.ratio)})})}function dc(ey,ex){d6.trigger(ag+":"+ey,[cg,ex])}function dR(){clearTimeout(cr.t);c6=1;if(c3.stopautoplayontouch){cg.stopAutoplay()}else{cj=true}}function cr(){if(!c6){return}if(!c3.stopautoplayontouch){cw();es()}cr.t=setTimeout(function(){c6=0},ba+b8)}function cw(){cj=!!(dg||el)}function es(){clearTimeout(es.t);bX.stop(es.w);if(!c3.autoplay||cj){if(cg.autoplay){cg.autoplay=false;dc("stopautoplay")}return}if(!cg.autoplay){cg.autoplay=true;dc("startautoplay")}var ey=eo;var ex=dF[bu].data();es.w=bX(function(){return ex.state||ey!==eo},function(){es.t=setTimeout(function(){if(cj||ey!==eo){return}var ez=cK,eA=dP[ez][bu].data();es.w=bX(function(){return eA.state||ez!==cK},function(){if(cj||ez!==cK){return}cg.show(dj?ay(!dH):cK)})},c3.autoplay)})}cg.startAutoplay=function(ex){if(cg.autoplay){return this}cj=el=false;dq(ex||c3.autoplay);es();return this};cg.stopAutoplay=function(){if(cg.autoplay){cj=el=true;es()}return this};cg.showSlide=function(ez){var eA=aa(cF,c3.navdir),eC,eB=500*1.1,ey=c3.navdir==="horizontal"?c3.thumbwidth:c3.thumbheight,ex=function(){ev()};if(ez==="next"){eC=eA-(ey+c3.margin)*b4}if(ez==="prev"){eC=eA+(ey+c3.margin)*b4}eC=a5(eC,dy);dp(eC,true);A(cF,{time:eB,pos:eC,direction:c3.navdir,onEnd:ex})};cg.showWhileLongPress=function(eA){if(cg.longPress.singlePressInProgress){return}var ez=dK(eA);ew(ez);var eB=cA(eA)/50;var ey=dF;cg.activeFrame=dF=dP[eo];var ex=ey===dF&&!eA.user;cg.showNav(ex,eA,eB);return this};cg.showEndLongPress=function(eA){if(cg.longPress.singlePressInProgress){return}var ez=dK(eA);ew(ez);var eB=cA(eA)/50;var ey=dF;cg.activeFrame=dF=dP[eo];var ex=ey===dF&&!eA.user;cg.showStage(ex,eA,eB);ee=typeof dw!=="undefined"&&dw!==eo;dw=eo;return this};function dK(ey){var ex;if(typeof ey!=="object"){ex=ey;ey={}}else{ex=ey.index}ex=ex===">"?ed+1:ex==="<"?ed-1:ex==="<<"?0:ex===">>"?c1-1:ex;ex=isNaN(ex)?aP:ex;ex=typeof ex==="undefined"?eo||0:ex;return ex}function ew(ex){cg.activeIndex=eo=du(ex);d4=dB(eo);cN=ci(eo);cK=cZ(eo+(dH?-1:1));ea=[eo,d4,cN];ed=dj?ex:eo}function cA(ey){var ex=Math.abs(dw-ed),ez=bj(ey.time,function(){return Math.min(ds*(1+(ex-1)/12),ds*2)});if(ey.slow){ez*=10}return ez}cg.showStage=function(ey,eA,eD){cO(dg,dF.i!==dP[cZ(c2)].i);dl(ea,"stage");di(bh?[ed]:[ed,dB(ed),ci(ed)]);cD("go",true);ey||dc("show",{user:eA.user,time:eD});cj=true;var eC=eA.overPos;var ez=cg.showStage.onEnd=function(eE){if(ez.ok){return}ez.ok=true;eE||dC(true);if(!ey){dc("showend",{user:eA.user})}if(!eE&&cu&&cu!==c3.transition){cg.setOptions({transition:cu});cu=false;return}cy();cx(ea,"stage");cD("go",false);cJ();ei();cw();es();if(cg.fullScreen){dF[bu].find("."+Y).attr("aria-hidden",false);dF[bu].find("."+J).attr("aria-hidden",true)}else{dF[bu].find("."+Y).attr("aria-hidden",true);dF[bu].find("."+J).attr("aria-hidden",false)}};if(!cv){A(cl,{pos:-a8(ed,en.w,c3.margin,c2),overPos:eC,time:eD,onEnd:ez})}else{var ex=dF[bu],eB=dP[dw]&&eo!==dw?dP[dw][bu]:null;aq(ex,eB,c8,{time:eD,method:c3.transition,onEnd:ez},cI)}dh()};cg.showNav=function(ey,ez,eA){ev();if(cq){cS();var ex=cn(eo+bb(ed-dw,-1,1));dT({time:eA,coo:ex!==eo&&ez.coo,guessIndex:typeof ez.coo!=="undefined"?ex:eo,keep:ey});if(c0){d7(eA)}}};cg.show=function(eA){cg.longPress.singlePressInProgress=true;var ez=dK(eA);ew(ez);var eB=cA(eA);var ey=dF;cg.activeFrame=dF=dP[eo];var ex=ey===dF&&!eA.user;cg.showStage(ex,eA,eB);cg.showNav(ex,eA,eB);ee=typeof dw!=="undefined"&&dw!==eo;dw=eo;cg.longPress.singlePressInProgress=false;return this};cg.requestFullScreen=function(){if(c4&&!cg.fullScreen){var ex=bV((cg.activeFrame||{}).$stageFrame||{}).hasClass("fotorama-video-container");if(ex){return}cs=bf.scrollTop();cT=bf.scrollLeft();bU(bf);cD("x",true);dZ=bV.extend({},en);d6.addClass(M).appendTo(I.addClass(bH));R.addClass(bH);cO(dg,true,true);cg.fullScreen=true;if(d2){bB.request(eu)}cg.resize();cx(ea,"stage");cy();dc("fullscreenenter");if(!("ontouchstart" in bo)){dM.focus()}}return this};function cP(){if(cg.fullScreen){cg.fullScreen=false;if(s){bB.cancel(eu)}I.removeClass(bH);R.removeClass(bH);d6.removeClass(M).insertAfter(c9);en=bV.extend({},dZ);cO(dg,true,true);cD("x",false);cg.resize();cx(ea,"stage");bU(bf,cT,cs);dc("fullscreenexit")}}cg.cancelFullScreen=function(){if(d2&&bB.is()){bB.cancel(k)}else{cP()}return this};cg.toggleFullScreen=function(){return cg[(cg.fullScreen?"cancel":"request")+"FullScreen"]()};cg.resize=function(ez){if(!dP){return this}var eC=arguments[1]||0,ey=arguments[2];b4=ck(dk,c3);dL(!cg.fullScreen?bA(ez):{width:bV(bo).width(),maxwidth:null,minwidth:null,height:bV(bo).height(),maxheight:null,minheight:null},[en,ey||cg.fullScreen||c3]);var eB=en.width,ex=en.height,eA=en.ratio,eD=bf.height()-(cq?dO.height():0);if(t(eB)){dk.css({width:""});dk.css({height:""});cf.css({width:""});cf.css({height:""});cl.css({width:""});cl.css({height:""});dO.css({width:""});dO.css({height:""});dk.css({minWidth:en.minwidth||0,maxWidth:en.maxwidth||bK});if(cq==="dots"){dU.hide()}eB=en.W=en.w=dk.width();en.nw=cq&&d(c3.navwidth,eB)||eB;cl.css({width:en.w,marginLeft:(en.W-en.w)/2});ex=d(ex,eD);ex=ex||(eA&&eB/eA);if(ex){eB=Math.round(eB);ex=en.h=Math.round(bb(ex,d(en.minheight,eD),d(en.maxheight,eD)));cf.css({width:eB,height:ex});if(c3.navdir==="vertical"&&!cg.fullscreen){dO.width(c3.thumbwidth+c3.thumbmargin*2)}if(c3.navdir==="horizontal"&&!cg.fullscreen){dO.height(c3.thumbheight+c3.thumbmargin*2)}if(cq==="dots"){dO.width(eB).height("auto");dU.show()}if(c3.navdir==="vertical"&&cg.fullScreen){cf.css("height",bf.height())}if(c3.navdir==="horizontal"&&cg.fullScreen){cf.css("height",bf.height()-dO.height())}if(cq){switch(c3.navdir){case"vertical":dU.removeClass(bZ);dU.removeClass(ax);dU.addClass(b);dO.stop().animate({height:en.h,width:c3.thumbwidth},eC);break;case"list":dU.removeClass(b);dU.removeClass(bZ);dU.addClass(ax);break;default:dU.removeClass(b);dU.removeClass(ax);dU.addClass(bZ);dO.stop().animate({width:en.nw},eC);break}dC();dT({guessIndex:eo,time:eC,keep:true});if(c0&&cz.nav){d7(eC)}}dG=ey||true;eq.ok=true;eq()}}d3=cf.offset().left;i();return this};cg.setOptions=function(ex){bV.extend(c3,ex);dV();return this};cg.shuffle=function(){dP&&aC(dP)&&dV();return this};function co(ex,ey){if(ec){ex.removeClass(S+" "+aL);ex.removeClass(a2+" "+aR);ey&&!dg&&ex.addClass(ey.replace(/^|\s/g," "+bz+"--"))}}cg.longPress={threshold:1,count:0,thumbSlideTime:20,progress:function(){if(!this.inProgress){this.count++;this.inProgress=this.count>this.threshold}},end:function(){if(this.inProgress){this.isEnded=true}},reset:function(){this.count=0;this.inProgress=false;this.isEnded=false}};cg.destroy=function(){cg.cancelFullScreen();cg.stopAutoplay();dP=cg.data=null;dd();ea=[];ce(bu);dV.ok=false;return this};cg.playVideo=function(){var ez=dF,ex=ez.video,ey=eo;if(typeof ex==="object"&&ez.videoReady){d2&&cg.fullScreen&&cg.cancelFullScreen();bX(function(){return !bB.is()||ey!==eo},function(){if(ey===eo){ez.$video=ez.$video||bV(ab(bJ)).append(q(ex));ez.$video.appendTo(ez[bu]);dk.addClass(bt);dg=ez.$video;em();cU.blur();dM.blur();dc("loadvideo")}})}return this};cg.stopVideo=function(){cO(dg,true,true);return this};cg.spliceByIndex=function(ex,ey){ey.i=ex+1;ey.img&&bV.ajax({url:ey.img,type:"HEAD",success:function(){dP.splice(ex,1,ey);dV()}})};function cO(ex,ez,ey){if(ez){dk.removeClass(bt);dg=false;em()}if(ex&&ex!==dg){ex.remove();dc("unloadvideo")}if(ey){cw();es()}}function cp(ex){dk.toggleClass(P,ex)}function ei(ez){if(d5.flow){return}var ex=ez?ez.pageX:ei.x,ey=ex&&!cE(eh(ex))&&c3.click;if(ei.p!==ey&&cf.toggleClass(bC,ey)){ei.p=ey;ei.x=ex}}cf.on("mousemove",ei);function cm(ex){clearTimeout(cm.t);if(c3.clicktransition&&c3.clicktransition!==c3.transition){setTimeout(function(){var ey=c3.transition;cg.setOptions({transition:c3.clicktransition});cu=ey;cm.t=setTimeout(function(){cg.show(ex)},10)},0)}else{cg.show(ex)}}function ct(eA,ey){var ez=eA.target,ex=bV(ez);if(ex.hasClass(T)){cg.playVideo()}else{if(ez===dD){cg.toggleFullScreen()}else{if(dg){ez===d1&&cO(dg,true,true)}else{if(!d6.hasClass(M)){cg.requestFullScreen()}}}}O(eA,true)}function cD(ex,ey){d5[ex]=dy[ex]=ey}d5=ao(cl,{onStart:dR,onMove:function(ey,ex){co(cf,ex.edge)},onTouchEnd:cr,onEnd:function(ex){var ez;co(cf);ez=(aZ&&!dz||ex.touch)&&c3.arrows;if((ex.moved||(ez&&ex.pos!==ex.newPos&&!ex.control))&&ex.$target[0]!==dM[0]){var ey=by(ex.newPos,en.w,c3.margin,c2);cg.show({index:ey,time:cv?ds:ex.time,overPos:ex.overPos,user:true})}else{if(!ex.aborted&&!ex.control){ct(ex.startEvent,ez)}}},timeLow:1,timeHigh:1,friction:2,select:"."+a9+", ."+a9+" *",$wrap:cf,direction:"horizontal"});dy=ao(cF,{onStart:dR,onMove:function(ey,ex){co(dO,ex.edge)},onTouchEnd:cr,onEnd:function(ex){function ey(){dT.l=ex.newPos;cw();es();dp(ex.newPos,true);ev()}if(!ex.moved){var ez=ex.$target.closest("."+aG,cF)[0];ez&&dJ.call(ez,ex.startEvent)}else{if(ex.pos!==ex.newPos){cj=true;A(cF,{time:ex.time,pos:ex.newPos,overPos:ex.overPos,direction:c3.navdir,onEnd:ey});dp(ex.newPos);ec&&co(dO,bp(ex.newPos,dy.min,dy.max,ex.dir))}else{ey()}}},timeLow:0.5,timeHigh:2,friction:5,$wrap:dO,direction:c3.navdir});cG=o(cf,{shift:true,onEnd:function(ey,ex){dR();cr();cg.show({index:ex,slow:ey.altKey})}});ef=o(dO,{onEnd:function(ez,ey){dR();cr();var ex=a1(cF)+ey*0.25;cF.css(b2(bb(ex,dy.min,dy.max),c3.navdir));ec&&co(dO,bp(ex,dy.min,dy.max,c3.navdir));ef.prevent={"<":ex>=dy.max,">":ex<=dy.min};clearTimeout(ef.t);ef.t=setTimeout(function(){dT.l=ex;dp(ex,true)},b8);dp(ex)}});dk.hover(function(){setTimeout(function(){if(c6){return}cp(!(dz=true))},0)},function(){if(!dz){return}cp(!(dz=false))});function dJ(ey){var ex=bV(this).data().eq;if(c3.navtype==="thumbs"){cm({index:ex,slow:ey.altKey,user:true,coo:ey._x-dO.offset().left})}else{cm({index:ex,slow:ey.altKey,user:true})}}function et(ex){cm({index:cU.index(this)?">":"<",slow:ex.altKey,user:true})}z(cU,function(ex){O(ex);et.call(this,ex)},{onStart:function(){dR();d5.control=true},onTouchEnd:cr});z(eg,function(ex){O(ex);if(c3.navtype==="thumbs"){cg.show("<")}else{cg.showSlide("prev")}});z(dX,function(ex){O(ex);if(c3.navtype==="thumbs"){cg.show(">")}else{cg.showSlide("next")}});function dv(ex){bL(ex,function(){setTimeout(function(){bU(cf)},0);cp(false)})}cU.each(function(){a(this,function(ex){et.call(this,ex)});dv(this)});a(dD,function(){if(d6.hasClass(M)){cg.cancelFullScreen();cl.focus()}else{cg.requestFullScreen();dM.focus()}});dv(dD);function dV(){dn();dQ();if(!dV.i){dV.i=true;var ex=c3.startindex;eo=c2=ed=dw=dx=du(ex)||0}if(c1){if(cV()){return}if(dg){cO(dg,true)}ea=[];ce(bu);dV.ok=true;cg.show({index:eo,time:0});cg.resize()}else{cg.destroy()}}function cV(){if(!cV.f===dH){cV.f=dH;eo=c1-1-eo;cg.reverse();return true}}bV.each("load push pop shift unshift reverse sort splice".split(" "),function(ex,ey){cg[ey]=function(){dP=dP||[];if(ey!=="load"){Array.prototype[ey].apply(dP,arguments)}else{if(arguments[0]&&typeof arguments[0]==="object"&&arguments[0].length){dP=bG(arguments[0])}}dV();return cg}});function eq(){if(eq.ok){eq.ok=false;dc("ready")}}dV()};bV.fn.fotorama=function(i){return this.each(function(){var ce=this,cd=bV(this),cc=cd.data(),cf=cc.fotorama;if(!cf){bX(function(){return !W(ce)},function(){cc.urtext=cd.html();new bV.Fotorama(cd,bV.extend({},Q,bo.fotoramaDefaults,i,cc))})}else{cf.setOptions(i,true)}})};bV.Fotorama.instances=[];function b1(){bV.each(bV.Fotorama.instances,function(cc,i){i.index=cc})}function C(i){bV.Fotorama.instances.push(i);b1()}function av(i){bV.Fotorama.instances.splice(i.index,1);b1()}bV.Fotorama.cache={};bV.Fotorama.measures={};bV=bV||{};bV.Fotorama=bV.Fotorama||{};bV.Fotorama.jst=bV.Fotorama.jst||{};bV.Fotorama.jst.dots=function(cc){var i,ce="",cd=bx.escape;ce+='
\r\n
\r\n
';return ce};bV.Fotorama.jst.frameCaption=function(cc){var i,ce="",cd=bx.escape;ce+='\r\n";return ce};bV.Fotorama.jst.style=function(cc){var i,ce="",cd=bx.escape;ce+=".fotorama"+((i=(cc.s))==null?"":i)+" .fotorama__nav--thumbs .fotorama__nav__frame{\r\npadding:"+((i=(cc.m))==null?"":i)+"px;\r\nheight:"+((i=(cc.h))==null?"":i)+"px}\r\n.fotorama"+((i=(cc.s))==null?"":i)+" .fotorama__thumb-border{\r\nheight:"+((i=(cc.h))==null?"":i)+"px;\r\nborder-width:"+((i=(cc.b))==null?"":i)+"px;\r\nmargin-top:"+((i=(cc.m))==null?"":i)+"px}";return ce};bV.Fotorama.jst.thumb=function(cc){var i,ce="",cd=bx.escape;ce+='
\r\n
\r\n
\r\n
';return ce}})(window,document,location,typeof jQuery!=="undefined"&&jQuery); +fotoramaVersion="4.6.4",function(t,e,n,o,a){"use strict";var i="fotorama",r="fotorama__fullscreen",s=i+"__wrap",u=s+"--css2",l=s+"--css3",c=s+"--video",d=s+"--fade",h=s+"--slide",f=s+"--no-controls",m=s+"--no-shadows",v=s+"--pan-y",p=s+"--rtl",g=s+"--no-captions",w=s+"--toggle-arrows",b=i+"__stage",y=b+"__frame",x=y+"--video",_=b+"__shaft",C=i+"__grab",k=i+"__pointer",P=i+"__arr",S=P+"--disabled",T=P+"--prev",F=P+"--next",E=i+"__nav",M=E+"-wrap",j=E+"__shaft",$=M+"--vertical",z=M+"--list",q=M+"--horizontal",N=E+"--dots",A=E+"--thumbs",L=E+"__frame",O=i+"__fade",D=O+"-front",I=O+"-rear",W=i+"__shadow"+"s",R=W+"--left",H=W+"--right",K=W+"--top",Q=W+"--bottom",V=i+"__active",X=i+"__select",B=i+"--hidden",Y=i+"--fullscreen",U=i+"__fullscreen-icon",G=i+"__error",J=i+"__loading",Z=i+"__loaded",tt=Z+"--full",et=Z+"--img",nt=i+"__grabbing",ot=i+"__img",at=ot+"--full",it=i+"__thumb",rt=it+"__arr--left",st=it+"__arr--right",ut=it+"-border",lt=i+"__html",ct=i+"-video-container",dt=i+"__video",ht=dt+"-play",ft=dt+"-close",mt=i+"_horizontal_ratio",vt=i+"_vertical_ratio",pt=i+"__spinner",gt=pt+"--show",wt=o&&o.fn.jquery.split(".");if(!wt||wt[0]<1||1==wt[0]&&wt[1]<8)throw"Fotorama requires jQuery 1.8 or later and will not run without it.";var bt=function(t,e,n){var o,a,i={},r=e.documentElement,s="modernizr",u=e.createElement(s),l=u.style,c=" -webkit- -moz- -o- -ms- ".split(" "),d="Webkit Moz O ms",h=d.split(" "),f=d.toLowerCase().split(" "),m={},v=[],p=v.slice,g=function(t,n,o,a){var i,u,l,c,d=e.createElement("div"),h=e.body,f=h||e.createElement("body");if(parseInt(o,10))for(;o--;)(l=e.createElement("div")).id=a?a[o]:s+(o+1),d.appendChild(l);return i=["­",'"].join(""),d.id=s,(h?d:f).innerHTML+=i,f.appendChild(d),h||(f.style.background="",f.style.overflow="hidden",c=r.style.overflow,r.style.overflow="hidden",r.appendChild(f)),u=n(d,t),h?d.parentNode.removeChild(d):(f.parentNode.removeChild(f),r.style.overflow=c),!!u},w={}.hasOwnProperty;function b(t){l.cssText=t}function y(t,e){return typeof t===e}function x(t,e){for(var o in t){var a=t[o];if(!~(""+a).indexOf("-")&&l[a]!==n)return"pfx"!=e||a}return!1}function _(t,e,o){var a=t.charAt(0).toUpperCase()+t.slice(1),i=(t+" "+h.join(a+" ")+a).split(" ");return y(e,"string")||y(e,"undefined")?x(i,e):function(t,e,o){for(var a in t){var i=e[t[a]];if(i!==n)return!1===o?t[a]:y(i,"function")?i.bind(o||e):i}return!1}(i=(t+" "+f.join(a+" ")+a).split(" "),e,o)}for(var C in a=y(w,"undefined")||y(w.call,"undefined")?function(t,e){return e in t&&y(t.constructor.prototype[e],"undefined")}:function(t,e){return w.call(t,e)},Function.prototype.bind||(Function.prototype.bind=function(t){var e=this;if("function"!=typeof e)throw new TypeError;var n=p.call(arguments,1),o=function(){if(this instanceof o){var a=function(){};a.prototype=e.prototype;var i=new a,r=e.apply(i,n.concat(p.call(arguments)));return Object(r)===r?r:i}return e.apply(t,n.concat(p.call(arguments)))};return o}),m.touch=function(){var n;return"ontouchstart"in t||t.DocumentTouch&&e instanceof DocumentTouch?n=!0:g(["@media (",c.join("touch-enabled),("),s,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(t){n=9===t.offsetTop}),n},m.csstransforms3d=function(){var t=!!_("perspective");return t&&"webkitPerspective"in r.style&&g("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(e,n){t=9===e.offsetLeft&&3===e.offsetHeight}),t},m.csstransitions=function(){return _("transition")},m)a(m,C)&&(o=C.toLowerCase(),i[o]=m[C](),v.push((i[o]?"":"no-")+o));return i.addTest=function(t,e){if("object"==typeof t)for(var o in t)a(t,o)&&i.addTest(o,t[o]);else{if(t=t.toLowerCase(),i[t]!==n)return i;e="function"==typeof e?e():e,"undefined"!=typeof enableClasses&&enableClasses&&(r.className+=" "+(e?"":"no-")+t),i[t]=e}return i},b(""),u=null,i._version="2.8.3",i._prefixes=c,i._domPrefixes=f,i._cssomPrefixes=h,i.testProp=function(t){return x([t])},i.testAllProps=_,i.testStyles=g,i.prefixed=function(t,e,n){return e?_(t,e,n):_(t,"pfx")},i}(t,e),yt={ok:!1,is:function(){return!1},request:function(){},cancel:function(){},event:"",prefix:""},xt="webkit moz o ms khtml".split(" ");if(void 0!==e.cancelFullScreen)yt.ok=!0;else for(var _t=0,Ct=xt.length;_t=n?"bottom":"top bottom":t<=e?"left":t>=n?"right":"left right")}function be(t,e,n){n=n||{},t.each(function(){var t,a=o(this),i=a.data();i.clickOn||(i.clickOn=!0,o.extend(Re(a,{onStart:function(e){t=e,(n.onStart||Jt).call(this,e)},onMove:n.onMove||Jt,onTouchEnd:n.onTouchEnd||Jt,onEnd:function(n){n.moved||e.call(this,t)}}),{noMove:!0}))})}function ye(t,e){return'
'+(e||"")+"
"}function xe(t){return"."+t}function _e(t){for(var e=t.length;e;){var n=Math.floor(Math.random()*e--),o=t[e];t[e]=t[n],t[n]=o}return t}function Ce(t){return"[object Array]"==Object.prototype.toString.call(t)&&o.map(t,function(t){return o.extend({},t)})}function ke(t,e,n){t.scrollLeft(e||0).scrollTop(n||0)}function Pe(t){if(t){var e={};return o.each(t,function(t,n){e[t.toLowerCase()]=n}),e}}function Se(t){if(t){var e=+t;return isNaN(e)?+(e=t.split("/"))[0]/+e[1]||a:e}}function Te(t,e,n,o){e&&(t.addEventListener?t.addEventListener(e,n,!!o):t.attachEvent("on"+e,n))}function Fe(t,e){return t>e.max?t=e.max:t":"<"}pe.stop=function(t){pe.ii[t]=!1};var qe,Ne,Ae,Le,Oe,De=function(){return{setRatio:function(t,e,n){e/n<=1?(t.parent().removeClass(mt),t.parent().addClass(vt)):(t.parent().removeClass(vt),t.parent().addClass(mt))},setThumbAttr:function(t,e,n){var i=n;t.attr(i)||t.attr(i)===a||t.attr(i,e),t.find("["+i+"]").length&&t.find("["+i+"]").each(function(){o(this).attr(i,e)})},isExpectedCaption:function(t,e,n){var o,a=!1;return o=t.showCaption===n||!0===t.showCaption,!!e&&(t.caption&&o&&(a=!0),a)}}}(jQuery);function Ie(t,e){var n=t.data(),a=Math.round(e.pos),i=function(){n&&n.sliding&&(n.sliding=!1),(e.onEnd||Jt)()};void 0!==e.overPos&&e.overPos!==e.pos&&(a=e.overPos);var r=o.extend(ee(a,e.direction),e.width&&{width:e.width},e.height&&{height:e.height});n&&n.sliding&&(n.sliding=!0),Mt?(t.css(o.extend(ne(e.time),r)),e.time>10?ue(t,"transform",i,e.time):i()):t.stop().animate(r,e.time,Bt,i)}function We(t){var e=(t.touches||[])[0]||t;t._x=e.pageX||e.originalEvent.pageX,t._y=e.clientY||e.originalEvent.clientY,t._now=o.now()}function Re(t,n){var a,i,r,s,u,l,c,d,h,f=t[0],m={};function v(t){if(r=o(t.target),m.checked=l=c=h=!1,a||m.flow||t.touches&&t.touches.length>1||t.which>1||qe&&qe.type!==t.type&&Ae||(l=n.select&&r.is(n.select,f)))return l;u="touchstart"===t.type,c=r.is("a, a *",f),s=m.control,d=m.noMove||m.noSwipe||s?16:m.snap?0:4,We(t),i=qe=t,Ne=t.type.replace(/down|start/,"move").replace(/Down/,"Move"),(n.onStart||Jt).call(f,t,{control:s,$target:r}),a=m.flow=!0,u&&!m.go||$e(t)}function p(t){if(t.touches&&t.touches.length>1||Nt&&!t.isPrimary||Ne!==t.type||!a)return a&&w(),void(n.onTouchEnd||Jt)();We(t);var e=Math.abs(t._x-i._x),o=Math.abs(t._y-i._y),r=e-o,s=(m.go||m.x||r>=0)&&!m.noSwipe,l=r<0;u&&!m.checked?(a=s)&&$e(t):($e(t),g(e,o)&&(n.onMove||Jt).call(f,t,{touch:u})),!h&&g(e,o)&&Math.sqrt(Math.pow(e,2)+Math.pow(o,2))>d&&(h=!0),m.checked=m.checked||s||l}function g(t,e){return t>e&&t>1.5}function w(t){(n.onTouchEnd||Jt)();var e=a;m.control=a=!1,e&&(m.flow=!1),!e||c&&!m.checked||(t&&$e(t),Ae=!0,clearTimeout(Le),Le=setTimeout(function(){Ae=!1},1e3),(n.onEnd||Jt).call(f,{moved:h,$target:r,control:s,touch:u,startEvent:i,aborted:!t||"MSPointerCancel"===t.type}))}function b(){m.flow&&(m.flow=!1)}return Nt?(Te(f,"MSPointerDown",v),Te(e,"MSPointerMove",p),Te(e,"MSPointerCancel",w),Te(e,"MSPointerUp",w)):(Te(f,"touchstart",v),Te(f,"touchmove",p),Te(f,"touchend",w),Te(e,"touchstart",function(){m.flow||(m.flow=!0)}),Te(e,"touchend",b),Te(e,"touchcancel",b),St.on("scroll",b),t.on("mousedown pointerdown",v),Tt.on("mousemove pointermove",p).on("mouseup pointerup",w)),Oe=bt.touch?"a":"div",t.on("click",Oe,function(t){m.checked&&$e(t)}),m}function He(t,e){var n,a,i,r,s,u,l,c,d,h,f,m,v,p,g,w=t[0],b=t.data(),y={};function x(o,s){g=!0,n=a="vertical"===m?o._y:o._x,l=o._now,u=[[l,n]],i=r=y.noMove||s?0:le(t,(e.getPos||Jt)()),(e.onStart||Jt).call(w,o)}return y=o.extend(Re(e.$wrap,o.extend({},e,{onStart:function(e,n){d=y.min,h=y.max,f=y.snap,m=y.direction||"horizontal",t.navdir=m,v=e.altKey,g=p=!1,n.control||b.sliding||x(e)},onMove:function(o,l){y.noSwipe||(g||x(o),a="vertical"===m?o._y:o._x,u.push([o._now,a]),s=we(r=i-(n-a),d,h,m),r<=d?r=de(r,d):r>=h&&(r=de(r,h)),y.noMove||(t.css(ee(r,m)),p||(p=!0,l.touch||Nt||t.addClass(nt)),(e.onMove||Jt).call(w,o,{pos:r,edge:s})))},onEnd:function(n){if(!y.noSwipe||!n.moved){g||x(n.startEvent,!0),n.touch||Nt||t.removeClass(nt);for(var s,l,p,b,_,C,k,P,S,T=(c=o.now())-Lt,F=null,E=Ot,M=e.friction,j=u.length-1;j>=0;j--){if(s=u[j][0],l=Math.abs(s-T),null===F||lp)break;p=l}k=Zt(r,d,h);var $=b-a,z=$>=0,q=c-F,N=q>Lt,A=!N&&r!==i&&k===r;f&&(k=Zt(Math[A?z?"floor":"ceil":"round"](r/f)*f,d,h),d=h=k),A&&(f||k===r)&&(S=-$/q,E*=Zt(Math.abs(S),e.timeLow,e.timeHigh),_=Math.round(r+S*E/M),f||(k=_),(!z&&_>h||z&&_"),ln=o(ye(B)),cn=n.find(xe(s)),dn=cn.find(xe(b)),hn=(dn[0],n.find(xe(_))),fn=o(),mn=n.find(xe(T)),vn=n.find(xe(F)),pn=n.find(xe(P)),gn=n.find(xe(M)),wn=gn.find(xe(E)),bn=wn.find(xe(j)),yn=o(),xn=o(),_n=(hn.data(),bn.data(),n.find(xe(ut))),Cn=n.find(xe(rt)),kn=n.find(xe(st)),Pn=n.find(xe(U)),Sn=Pn[0],Tn=o(ye(ht)),Fn=n.find(xe(ft))[0],En=n.find(xe(pt)),Mn=!1,jn={},$n={},zn={},qn={},Nn={},An={},Ln={},On=0,Dn=[];function In(){o.each(nt,function(t,e){if(!e.i){e.i=rn++;var n=fe(e.video,!0);if(n){var a={};e.video=n,e.img||e.thumb?e.thumbsReady=!0:(r=nt,s=en,"youtube"===(c=(i=e).video).type?(u=(l=he()+"img.youtube.com/vi/"+c.id+"/default.jpg").replace(/\/default.jpg$/,"/hqdefault.jpg"),i.thumbsReady=!0):"vimeo"===c.type?o.ajax({url:he()+"vimeo.com/api/v2/video/"+c.id+".json",dataType:"jsonp",success:function(t){i.thumbsReady=!0,me(r,{img:t[0].thumbnail_large,thumb:t[0].thumbnail_small},i.i,s)}}):i.thumbsReady=!0,a={img:u,thumb:l}),me(nt,{img:a.img,thumb:a.thumb},e.i,en)}}var i,r,s,u,l,c})}function Wn(t){return Re[t]}function Rn(){if(dn!==a)if("vertical"==O.navdir){var t=O.thumbwidth+O.thumbmargin;dn.css("left",t),vn.css("right",t),Pn.css("right",t),cn.css("width",cn.css("width")+t),hn.css("max-width",cn.width()-t)}else dn.css("left",""),vn.css("right",""),Pn.css("right",""),cn.css("width",cn.css("width")+t),hn.css("max-width","")}function Hn(t){var e,a,s,u,l,c,d,h,f;t!==Hn.f&&(t?(n.addClass(i+" "+on).before(ln).before(un),a=en,o.Fotorama.instances.push(a),Qe()):(ln.detach(),un.detach(),n.html(sn.urtext).removeClass(on),e=en,o.Fotorama.instances.splice(e.index,1),Qe()),l="keydown."+i,d="keydown."+(c=i+nn),h="keyup."+c,f="resize."+c+" orientationchange."+c,(s=t)?(Tt.on(d,function(t){var e,n;vt&&27===t.keyCode?(e=!0,Mo(vt,!0,!0)):(en.fullScreen||O.keyboard&&!en.index)&&(27===t.keyCode?(e=!0,en.cancelFullScreen()):t.shiftKey&&32===t.keyCode&&Wn("space")||37===t.keyCode&&Wn("left")||38===t.keyCode&&Wn("up")&&o(":focus").attr("data-gallery-role")?(en.longPress.progress(),n="<"):32===t.keyCode&&Wn("space")||39===t.keyCode&&Wn("right")||40===t.keyCode&&Wn("down")&&o(":focus").attr("data-gallery-role")?(en.longPress.progress(),n=">"):36===t.keyCode&&Wn("home")?(en.longPress.progress(),n="<<"):35===t.keyCode&&Wn("end")&&(en.longPress.progress(),n=">>")),(e||n)&&$e(t),u={index:n,slow:t.altKey,user:!0},n&&(en.longPress.inProgress?en.showWhileLongPress(u):en.show(u))}),s&&Tt.on(h,function(t){en.longPress.inProgress&&en.showEndLongPress({user:!0}),en.longPress.reset()}),en.index||Tt.off(l).on(l,"textarea, input, select",function(t){!Pt.hasClass(r)&&t.stopPropagation()}),St.on(f,en.resize)):(Tt.off(d),St.off(f)),Hn.f=t)}function Kn(){var t=it<2||vt;$n.noMove=t||Te,$n.noSwipe=t||!O.swipe,!Le&&hn.toggleClass(C,!O.click&&!$n.noMove&&!$n.noSwipe),Nt&&cn.toggleClass(v,!$n.noSwipe)}function Qn(t){!0===t&&(t=""),O.autoplay=Math.max(+t||It,1.5*Ae)}function Vn(t,e){return Math.floor(cn.width()/(e.thumbwidth+e.thumbmargin))}function Xn(){var t;O.nav&&"dots"!==O.nav||(O.navdir="horizontal"),en.options=O=Pe(O),Yt=Vn(0,O),Te="crossfade"===O.transition||"dissolve"===O.transition,Dt=O.loop&&(it>2||Te&&(!Le||"slide"!==Le)),Ae=+O.transitionduration||Ot,We="rtl"===O.direction,Re=o.extend({},O.keyboard&&Gt,O.keyboard),(t=O).navarrows&&"thumbs"===t.nav?(Cn.show(),kn.show()):(Cn.hide(),kn.hide());var e,n,a,i,r={add:[],remove:[]};function s(t,e){r[t?"add":"remove"].push(e)}it>1?(Bt=O.nav,oe="top"===O.navposition,r.remove.push(X),pn.toggle(O.arrows)):(Bt=!1,pn.hide()),co(),fo(),ho(),O.autoplay&&Qn(O.autoplay),qe=ae(O.thumbwidth)||Wt,Ne=ae(O.thumbheight)||Wt,zn.ok=Nn.ok=O.trackpad&&!qt,Kn(),yo(O,[jn]),Ut="thumbs"===Bt,gn.filter(":hidden")&&Bt&&gn.show(),Ut?(ao(it,"navThumb"),mt=xn,tn=Vt,e=un,n=o.Fotorama.jst.style({w:qe,h:Ne,b:O.thumbborderwidth,m:O.thumbmargin,s:nn,q:!jt}),(a=e[0]).styleSheet?a.styleSheet.cssText=n:e.html(n),wn.addClass(A).removeClass(N)):"dots"===Bt?(ao(it,"navDot"),mt=yn,tn=Qt,wn.addClass(N).removeClass(A)):(gn.hide(),Bt=!1,wn.removeClass(A+" "+N)),Bt&&(oe?gn.insertBefore(dn):gn.insertAfter(dn),uo.nav=!1,uo(mt,bn,"nav")),(ue=O.allowfullscreen)?(Pn.prependTo(dn),de=$t&&"native"===ue,i="touchend",Pn.on(i,function(t){return $e(t,!0),!1})):(Pn.detach(),de=!1),s(Te,d),s(!Te,h),s(!O.captions,g),s(We,p),s(O.arrows,w),s(!(Oe=O.shadows&&!qt),m),cn.addClass(r.add.join(" ")).removeClass(r.remove.join(" ")),o.extend({},O),Rn()}function Bn(t){return t<0?(it+t%it)%it:t>=it?t%it:t}function Yn(t){return Zt(t,0,it-1)}function Un(t){return Dt?Bn(t):Yn(t)}function Gn(t){return!!(t>0||Dt)&&t-1}function Jn(t){return!!(t1&&nt[i]===r)||r.html||r.deleted||r.video||c||(r.deleted=!0,en.splice(i,1))):(r[m]=v=p,l.$full=null,eo([i],e,n,!0))}function b(){var t=10;pe(function(){return!Je||!t--&&!qt},function(){o.Fotorama.measures[v]=f.measures=o.Fotorama.measures[v]||{width:d.width,height:d.height,ratio:d.width/d.height},to(f.measures.width,f.measures.height,f.measures.ratio,i),h.off("load error").addClass(""+(c?at:ot)).attr("aria-hidden","false").prependTo(s),s.hasClass(y)&&!s.hasClass(ct)&&s.attr("href",h.attr("src")),ge(h,(o.isFunction(n)?n():n)||jn),o.Fotorama.cache[v]=l.state="loaded",setTimeout(function(){s.trigger("f:load").removeClass(J+" "+G).addClass(Z+" "+(c?tt:et)),"stage"===e?g("load"):(r.thumbratio===Xt||!r.thumbratio&&O.thumbratio===Xt)&&(r.thumbratio=f.measures.ratio,Oo())},0)})}})}function no(){var t=wt[Kt];t&&!t.data().state&&(En.addClass(gt),t.on("f:load f:error",function(){t.off("f:load f:error"),En.removeClass(gt)}))}function oo(t){Me(t,No),je(t,function(){setTimeout(function(){ke(wn)},0),po({time:Ae,guessIndex:o(this).data().eq,minMax:qn})})}function ao(t,e){Zn(t,e,function(t,n,a,i,r,s){if(!i){i=a[r]=cn[r].clone(),(s=i.data()).data=a;var u=i[0],l="labelledby"+o.now();"stage"===e?(a.html&&o('
').append(a._html?o(a.html).removeAttr("id").html(a._html):a.html).appendTo(i),a.id&&(l=a.id||l),a.labelledby=l,De.isExpectedCaption(a,O.showcaption)&&o(o.Fotorama.jst.frameCaption({caption:a.caption,labelledby:l})).appendTo(i),a.video&&i.addClass(x).append(Tn.clone()),je(u,function(){setTimeout(function(){ke(dn)},0),zo({index:s.eq,user:!0})}),fn=fn.add(i)):"navDot"===e?(oo(u),yn=yn.add(i)):"navThumb"===e&&(oo(u),s.$wrap=i.children(":first"),xn=xn.add(i),a.video&&s.$wrap.append(Tn.clone()))}})}function io(t,e){return t&&t.length&&ge(t,e)}function ro(t){Zn(t,"stage",function(t,n,a,i,r,s){if(i){var u,l=Bn(n);s.eq=l,Ln[Kt][l]=i.css(o.extend({left:Te?0:se(n,jn.w,O.margin,xt)},Te&&ne(0))),u=i[0],o.contains(e.documentElement,u)||(i.appendTo(hn),Mo(a.$video)),io(s.$img,jn),io(s.$full,jn),!i.hasClass(y)||"false"===i.attr("aria-hidden")&&i.hasClass(V)||i.attr("aria-hidden","true")}})}function so(t,e){var n,a;"thumbs"!==Bt||isNaN(t)||(n=-t,a=-t+jn.nw,"vertical"===O.navdir&&(t-=O.thumbheight,a=-t+jn.h),xn.each(function(){var t=o(this).data(),i=t.eq,r=function(){return{h:Ne,w:t.w}},s=r(),u="vertical"===O.navdir?t.t>a:t.l>a;s.w=t.w,t.l+t.w=qn.max,e=n<=qn.min}Cn.toggleClass(S,t).attr(Ee(t,!0)),kn.toggleClass(S,e).attr(Ee(e,!0))}function fo(){zn.ok&&(zn.prevent={"<":lo(0),">":lo(1)})}function mo(t){var e,n,o,a,i=t.data();Ut?(e=i.l,n=i.t,o=i.w,a=i.h):(e=t.position().left,o=t.width());var r={c:e+o/2,min:-e+10*O.thumbmargin,max:-e+jn.w-o-10*O.thumbmargin},s={c:n+a/2,min:-n+10*O.thumbmargin,max:-n+jn.h-a-10*O.thumbmargin};return"vertical"===O.navdir?s:r}function vo(t){var e=wt[tn].data();Ie(_n,{time:1.2*t,pos:"vertical"===O.navdir?e.t:e.l,width:e.w,height:e.h,direction:O.navdir})}function po(t){var e,n,o,a,i,r,s,u,l,c,d,h,f,m,v,p,g,w=nt[t.guessIndex][tn],b=O.navtype;w&&("thumbs"===b?(e=qn.min!==qn.max,o=t.minMax||e&&mo(wt[tn]),a=e&&(t.keep&&po.t?po.l:Zt((t.coo||jn.nw/2)-mo(w).c,o.min,o.max)),i=e&&(t.keep&&po.l?po.l:Zt((t.coo||jn.nw/2)-mo(w).c,o.min,o.max)),r="vertical"===O.navdir?a:i,s=e&&Zt(r,qn.min,qn.max)||0,n=1.1*t.time,Ie(bn,{time:n,pos:s,direction:O.navdir,onEnd:function(){so(s,!0),ho()}}),Eo(wn,we(s,qn.min,qn.max,O.navdir)),po.l=r):(u=te(bn,O.navdir),n=1.11*t.time,l=O,c=qn,d=t.guessIndex,h=u,f=w,m=gn,"horizontal"===(v=O.navdir)?(p=l.thumbwidth,g=m.width()):(p=l.thumbheight,g=m.height()),s=Fe((p+l.margin)*(d+1)>=g-h?"horizontal"===v?-f.position().left:-f.position().top:(p+l.margin)*d<=Math.abs(h)?"horizontal"===v?-f.position().left+g-(p+l.margin):-f.position().top+g-(p+l.margin):h,c)||0,Ie(bn,{time:n,pos:s,direction:O.navdir,onEnd:function(){so(s,!0),ho()}}),Eo(wn,we(s,qn.min,qn.max,O.navdir))))}function go(t){for(var e=An[t];e.length;)e.shift().removeClass(V).attr("data-active",!1)}function wo(t){var e=Ln[t];o.each(bt,function(t,n){delete e[Bn(n)]}),o.each(e,function(t,n){delete e[t],n.detach()})}function bo(t){xt=_t=Mn;var e,o,a,i=wt[Kt];i&&(go(Kt),An[Kt].push(i.addClass(V).attr("data-active",!0)),i.hasClass(y)&&i.attr("aria-hidden","false"),t||en.showStage.onEnd(!0),le(hn,0),wo(Kt),ro(bt),$n.min=Dt?-1/0:-se(it-1,jn.w,O.margin,xt),$n.max=Dt?1/0:-se(0,jn.w,O.margin,xt),$n.snap=jn.w+O.margin,e="vertical"===O.navdir,o=e?bn.height():bn.width(),a=e?jn.h:jn.nw,qn.min=Math.min(0,a-o),qn.max=0,qn.direction=O.navdir,bn.toggleClass(C,!(qn.noMove=qn.min===qn.max)),Me(hn[0],function(){n.hasClass(Y)||(en.requestFullScreen(),Pn.focus())}))}function yo(t,e){t&&o.each(e,function(e,n){n&&o.extend(n,{width:t.width||n.width,height:t.height,minwidth:t.minwidth,maxwidth:t.maxwidth,minheight:t.minheight,maxheight:t.maxheight,ratio:Se(t.ratio)})})}function xo(t,e){n.trigger(i+":"+t,[en,e])}function _o(){clearTimeout(Co.t),Je=1,O.stopautoplayontouch?en.stopAutoplay():Ye=!0}function Co(){Je&&(O.stopautoplayontouch||(ko(),Po()),Co.t=setTimeout(function(){Je=0},Ot+Lt))}function ko(){Ye=!(!vt&&!Ue)}function Po(){if(clearTimeout(Po.t),pe.stop(Po.w),O.autoplay&&!Ye){en.autoplay||(en.autoplay=!0,xo("startautoplay"));var t=Mn,e=wt[Kt].data();Po.w=pe(function(){return e.state||t!==Mn},function(){Po.t=setTimeout(function(){if(!Ye&&t===Mn){var e=zt,n=nt[e][Kt].data();Po.w=pe(function(){return n.state||e!==zt},function(){Ye||e!==zt||en.show(Dt?ze(!We):zt)})}},O.autoplay)})}else en.autoplay&&(en.autoplay=!1,xo("stopautoplay"))}function So(t){var e;return"object"!=typeof t?(e=t,t={}):e=t.index,e=">"===e?_t+1:"<"===e?_t-1:"<<"===e?0:">>"===e?it-1:e,e=void 0===(e=isNaN(e)?a:e)?Mn||0:e}function To(t){en.activeIndex=Mn=Un(t),Ft=Gn(Mn),Et=Jn(Mn),zt=Bn(Mn+(We?-1:1)),bt=[Mn,Ft,Et],_t=Dt?t:Mn}function Fo(t){var e=Math.abs(Ct-_t),n=ce(t.time,function(){return Math.min(Ae*(1+(e-1)/12),2*Ae)});return t.slow&&(n*=10),n}function Eo(t,e){Oe&&(t.removeClass(R+" "+H),t.removeClass(K+" "+Q),e&&!vt&&t.addClass(e.replace(/^|\s/g," "+W+"--")))}function Mo(t,e,n){e&&(cn.removeClass(c),vt=!1,Kn()),t&&t!==vt&&(t.remove(),xo("unloadvideo")),n&&(ko(),Po())}function jo(t){cn.toggleClass(f,t)}function $o(t){if(!$n.flow){var e,n=t?t.pageX:$o.x,o=n&&!lo((e=n,e-On>jn.w/3))&&O.click;$o.p!==o&&dn.toggleClass(k,o)&&($o.p=o,$o.x=n)}}function zo(t){clearTimeout(zo.t),O.clicktransition&&O.clicktransition!==O.transition?setTimeout(function(){var e=O.transition;en.setOptions({transition:O.clicktransition}),Le=e,zo.t=setTimeout(function(){en.show(t)},10)},0):en.show(t)}function qo(t,e){$n[t]=qn[t]=e}function No(t){var e=o(this).data().eq;"thumbs"===O.navtype?zo({index:e,slow:t.altKey,user:!0,coo:t._x-wn.offset().left}):zo({index:e,slow:t.altKey,user:!0})}function Ao(t){zo({index:pn.index(this)?">":"<",slow:t.altKey,user:!0})}function Lo(t){je(t,function(){setTimeout(function(){ke(dn)},0),jo(!1)})}function Oo(){if(nt=en.data=nt||Ce(O.data)||ve(n),it=en.size=nt.length,Do.ok&&O.shuffle&&_e(nt),In(),Mn=Yn(Mn),it&&Hn(!0),Xn(),!Oo.i){Oo.i=!0;var t=O.startindex;Mn=xt=_t=Ct=At=Un(t)||0}if(it){if(function t(){if(!t.f===We)return t.f=We,Mn=it-1-Mn,en.reverse(),!0}())return;vt&&Mo(vt,!0),bt=[],wo(Kt),Oo.ok=!0,en.show({index:Mn,time:0}),en.resize()}else en.destroy()}function Do(){Do.ok&&(Do.ok=!1,xo("ready"))}cn[Kt]=o('
'),cn[Vt]=o(o.Fotorama.jst.thumb()),cn[Qt]=o(o.Fotorama.jst.dots()),An[Kt]=[],An[Vt]=[],An[Qt]=[],Ln[Kt]={},cn.addClass(Mt?l:u),sn.fotorama=this,en.startAutoplay=function(t){return en.autoplay?this:(Ye=Ue=!1,Qn(t||O.autoplay),Po(),this)},en.stopAutoplay=function(){return en.autoplay&&(Ye=Ue=!0,Po()),this},en.showSlide=function(t){var e,n=te(bn,O.navdir),o="horizontal"===O.navdir?O.thumbwidth:O.thumbheight;"next"===t&&(e=n-(o+O.margin)*Yt),"prev"===t&&(e=n+(o+O.margin)*Yt),so(e=Fe(e,qn),!0),Ie(bn,{time:550,pos:e,direction:O.navdir,onEnd:function(){ho()}})},en.showWhileLongPress=function(t){if(!en.longPress.singlePressInProgress){To(So(t));var e=Fo(t)/50,n=wt;en.activeFrame=wt=nt[Mn];var o=n===wt&&!t.user;return en.showNav(o,t,e),this}},en.showEndLongPress=function(t){if(!en.longPress.singlePressInProgress){To(So(t));var e=Fo(t)/50,n=wt;en.activeFrame=wt=nt[Mn];var o=n===wt&&!t.user;return en.showStage(o,t,e),void 0!==Ct&&Ct!==Mn,Ct=Mn,this}},en.showStage=function(t,e,n){Mo(vt,wt.i!==nt[Bn(xt)].i),ao(bt,"stage"),ro(qt?[_t]:[_t,Gn(_t),Jn(_t)]),qo("go",!0),t||xo("show",{user:e.user,time:n}),Ye=!0;var a=e.overPos,i=en.showStage.onEnd=function(n){if(!i.ok){if(i.ok=!0,n||bo(!0),t||xo("showend",{user:e.user}),!n&&Le&&Le!==O.transition)return en.setOptions({transition:Le}),void(Le=!1);no(),eo(bt,"stage"),qo("go",!1),fo(),$o(),ko(),Po(),en.fullScreen?(wt[Kt].find("."+at).attr("aria-hidden",!1),wt[Kt].find("."+ot).attr("aria-hidden",!0)):(wt[Kt].find("."+at).attr("aria-hidden",!0),wt[Kt].find("."+ot).attr("aria-hidden",!1))}};Te?function t(e,n,a,i,r,s){var u=void 0!==s;if(u||(r.push(arguments),Array.prototype.push.call(arguments,r.length),!(r.length>1))){e=e||o(e),n=n||o(n);var l=e[0],c=n[0],d="crossfade"===i.method,h=function(){if(!h.done){h.done=!0;var e=(u||r.shift())&&r.shift();e&&t.apply(this,e),(i.onEnd||Jt)(!!e)}},f=i.time/(s||1);a.removeClass(I+" "+D),e.stop().addClass(I),n.stop().addClass(D),d&&c&&e.fadeTo(0,0),e.fadeTo(d?f:0,1,d&&h),n.fadeTo(f,0,h),l&&d||c||h()}}(wt[Kt],nt[Ct]&&Mn!==Ct?nt[Ct][Kt]:null,fn,{time:n,method:O.transition,onEnd:i},Dn):Ie(hn,{pos:-se(_t,jn.w,O.margin,xt),overPos:a,time:n,onEnd:i});co()},en.showNav=function(t,e,n){if(ho(),Bt){go(tn),An[tn].push(wt[tn].addClass(V).attr("data-active",!0));var o=Yn(Mn+Zt(_t-Ct,-1,1));po({time:n,coo:o!==Mn&&e.coo,guessIndex:void 0!==e.coo?o:Mn,keep:t}),Ut&&vo(n)}},en.show=function(t){en.longPress.singlePressInProgress=!0,To(So(t));var e=Fo(t),n=wt;en.activeFrame=wt=nt[Mn];var o=n===wt&&!t.user;return en.showStage(o,t,e),en.showNav(o,t,e),void 0!==Ct&&Ct!==Mn,Ct=Mn,en.longPress.singlePressInProgress=!1,this},en.requestFullScreen=function(){if(ue&&!en.fullScreen){if(o((en.activeFrame||{}).$stageFrame||{}).hasClass("fotorama-video-container"))return;Xe=St.scrollTop(),Be=St.scrollLeft(),ke(St),qo("x",!0),Ge=o.extend({},jn),n.addClass(Y).appendTo(Pt.addClass(r)),kt.addClass(r),Mo(vt,!0,!0),en.fullScreen=!0,de&&yt.request(an),en.resize(),eo(bt,"stage"),no(),xo("fullscreenenter"),"ontouchstart"in t||Pn.focus()}return this},en.cancelFullScreen=function(){return de&&yt.is()?yt.cancel(e):en.fullScreen&&(en.fullScreen=!1,$t&&yt.cancel(an),Pt.removeClass(r),kt.removeClass(r),n.removeClass(Y).insertAfter(ln),jn=o.extend({},Ge),Mo(vt,!0,!0),qo("x",!1),en.resize(),eo(bt,"stage"),ke(St,Be,Xe),xo("fullscreenexit")),this},en.toggleFullScreen=function(){return en[(en.fullScreen?"cancel":"request")+"FullScreen"]()},en.resize=function(e){if(!nt)return this;var n=arguments[1]||0,a=arguments[2];Yt=Vn(0,O),yo(en.fullScreen?{width:o(t).width(),maxwidth:null,minwidth:null,height:o(t).height(),maxheight:null,minheight:null}:Pe(e),[jn,a||en.fullScreen||O]);var i=jn.width,r=jn.height,s=jn.ratio,u=St.height()-(Bt?wn.height():0);if(re(i)&&(cn.css({width:""}),cn.css({height:""}),dn.css({width:""}),dn.css({height:""}),hn.css({width:""}),hn.css({height:""}),wn.css({width:""}),wn.css({height:""}),cn.css({minWidth:jn.minwidth||0,maxWidth:jn.maxwidth||1200}),"dots"===Bt&&gn.hide(),i=jn.W=jn.w=cn.width(),jn.nw=Bt&&ie(O.navwidth,i)||i,hn.css({width:jn.w,marginLeft:(jn.W-jn.w)/2}),r=(r=ie(r,u))||s&&i/s)){if(i=Math.round(i),r=jn.h=Math.round(Zt(r,ie(jn.minheight,u),ie(jn.maxheight,u))),dn.css({width:i,height:r}),"vertical"!==O.navdir||en.fullscreen||wn.width(O.thumbwidth+2*O.thumbmargin),"horizontal"!==O.navdir||en.fullscreen||wn.height(O.thumbheight+2*O.thumbmargin),"dots"===Bt&&(wn.width(i).height("auto"),gn.show()),"vertical"===O.navdir&&en.fullScreen&&dn.css("height",St.height()),"horizontal"===O.navdir&&en.fullScreen&&dn.css("height",St.height()-wn.height()),Bt){switch(O.navdir){case"vertical":gn.removeClass(q),gn.removeClass(z),gn.addClass($),wn.stop().animate({height:jn.h,width:O.thumbwidth},n);break;case"list":gn.removeClass($),gn.removeClass(q),gn.addClass(z);break;default:gn.removeClass($),gn.removeClass(z),gn.addClass(q),wn.stop().animate({width:jn.nw},n)}bo(),po({guessIndex:Mn,time:n,keep:!0}),Ut&&uo.nav&&vo(n)}Ve=a||!0,Do.ok=!0,Do()}return On=dn.offset().left,Rn(),this},en.setOptions=function(t){return o.extend(O,t),Oo(),this},en.shuffle=function(){return nt&&_e(nt)&&Oo(),this},en.longPress={threshold:1,count:0,thumbSlideTime:20,progress:function(){this.inProgress||(this.count++,this.inProgress=this.count>this.threshold)},end:function(){this.inProgress&&(this.isEnded=!0)},reset:function(){this.count=0,this.inProgress=!1,this.isEnded=!1}},en.destroy=function(){return en.cancelFullScreen(),en.stopAutoplay(),nt=en.data=null,Hn(),bt=[],wo(Kt),Oo.ok=!1,this},en.playVideo=function(){var t=wt,e=t.video,n=Mn;return"object"==typeof e&&t.videoReady&&(de&&en.fullScreen&&en.cancelFullScreen(),pe(function(){return!yt.is()||n!==Mn},function(){var a;n===Mn&&(t.$video=t.$video||o(ye(dt)).append(''),t.$video.appendTo(t[Kt]),cn.addClass(c),vt=t.$video,Kn(),pn.blur(),Pn.blur(),xo("loadvideo"))})),this},en.stopVideo=function(){return Mo(vt,!0,!0),this},en.spliceByIndex=function(t,e){e.i=t+1,e.img&&o.ajax({url:e.img,type:"HEAD",success:function(){nt.splice(t,1,e),Oo()}})},dn.on("mousemove",$o),$n=He(hn,{onStart:_o,onMove:function(t,e){Eo(dn,e.edge)},onTouchEnd:Co,onEnd:function(t){var e,a,i,r,s,u,l;if(Eo(dn),e=(Nt&&!Ze||t.touch)&&O.arrows,(t.moved||e&&t.pos!==t.newPos&&!t.control)&&t.$target[0]!==Pn[0]){var c=(r=t.newPos,s=jn.w,u=O.margin,l=xt,-Math.round(r/(s+(u||0))-(l||0)));en.show({index:c,time:Te?Ae:t.time,overPos:t.overPos,user:!0})}else t.aborted||t.control||(a=t.startEvent,i=a.target,o(i).hasClass(ht)?en.playVideo():i===Sn?en.toggleFullScreen():vt?i===Fn&&Mo(vt,!0,!0):n.hasClass(Y)||en.requestFullScreen())},timeLow:1,timeHigh:1,friction:2,select:"."+X+", ."+X+" *",$wrap:dn,direction:"horizontal"}),qn=He(bn,{onStart:_o,onMove:function(t,e){Eo(wn,e.edge)},onTouchEnd:Co,onEnd:function(t){function e(){po.l=t.newPos,ko(),Po(),so(t.newPos,!0),ho()}if(t.moved)t.pos!==t.newPos?(Ye=!0,Ie(bn,{time:t.time,pos:t.newPos,overPos:t.overPos,direction:O.navdir,onEnd:e}),so(t.newPos),Oe&&Eo(wn,we(t.newPos,qn.min,qn.max,t.dir))):e();else{var n=t.$target.closest("."+L,bn)[0];n&&No.call(n,t.startEvent)}},timeLow:.5,timeHigh:2,friction:5,$wrap:wn,direction:O.navdir}),zn=Ke(dn,{shift:!0,onEnd:function(t,e){_o(),Co(),en.show({index:e,slow:t.altKey})}}),Nn=Ke(wn,{onEnd:function(t,e){_o(),Co();var n=le(bn)+.25*e;bn.css(ee(Zt(n,qn.min,qn.max),O.navdir)),Oe&&Eo(wn,we(n,qn.min,qn.max,O.navdir)),Nn.prevent={"<":n>=qn.max,">":n<=qn.min},clearTimeout(Nn.t),Nn.t=setTimeout(function(){po.l=n,so(n,!0)},Lt),so(n)}}),cn.hover(function(){setTimeout(function(){Je||jo(!(Ze=!0))},0)},function(){Ze&&jo(!(Ze=!1))}),be(pn,function(t){$e(t),Ao.call(this,t)},{onStart:function(){_o(),$n.control=!0},onTouchEnd:Co}),be(Cn,function(t){$e(t),"thumbs"===O.navtype?en.show("<"):en.showSlide("prev")}),be(kn,function(t){$e(t),"thumbs"===O.navtype?en.show(">"):en.showSlide("next")}),pn.each(function(){Me(this,function(t){Ao.call(this,t)}),Lo(this)}),Me(Sn,function(){n.hasClass(Y)?(en.cancelFullScreen(),hn.focus()):(en.requestFullScreen(),Pn.focus())}),Lo(Sn),o.each("load push pop shift unshift reverse sort splice".split(" "),function(t,e){en[e]=function(){return nt=nt||[],"load"!==e?Array.prototype[e].apply(nt,arguments):arguments[0]&&"object"==typeof arguments[0]&&arguments[0].length&&(nt=Ce(arguments[0])),Oo(),en}}),Oo()},o.fn.fotorama=function(e){return this.each(function(){var n=this,a=o(this),i=a.data(),r=i.fotorama;r?r.setOptions(e,!0):pe(function(){return!(0===(t=n).offsetWidth&&0===t.offsetHeight);var t},function(){i.urtext=a.html(),new o.Fotorama(a,o.extend({},Ut,t.fotoramaDefaults,e,i))})})},o.Fotorama.instances=[],o.Fotorama.cache={},o.Fotorama.measures={},(o=o||{}).Fotorama=o.Fotorama||{},o.Fotorama.jst=o.Fotorama.jst||{},o.Fotorama.jst.dots=function(t){return'
\r\n
\r\n
','
\r\n
\r\n
'},o.Fotorama.jst.frameCaption=function(t){var e,n="";return n+='\r\n"},o.Fotorama.jst.style=function(t){var e,n="";return n+=".fotorama"+(null==(e=t.s)?"":e)+" .fotorama__nav--thumbs .fotorama__nav__frame{\r\npadding:"+(null==(e=t.m)?"":e)+"px;\r\nheight:"+(null==(e=t.h)?"":e)+"px}\r\n.fotorama"+(null==(e=t.s)?"":e)+" .fotorama__thumb-border{\r\nheight:"+(null==(e=t.h)?"":e)+"px;\r\nborder-width:"+(null==(e=t.b)?"":e)+"px;\r\nmargin-top:"+(null==(e=t.m)?"":e)+"px}"},o.Fotorama.jst.thumb=function(t){return'
\r\n
\r\n
\r\n
','
\r\n
\r\n
\r\n
'}}(window,document,location,"undefined"!=typeof jQuery&&jQuery); \ No newline at end of file diff --git a/lib/web/mage/adminhtml/tools.js b/lib/web/mage/adminhtml/tools.js index ed4bab7102ae5..27f6efcfc5876 100644 --- a/lib/web/mage/adminhtml/tools.js +++ b/lib/web/mage/adminhtml/tools.js @@ -348,7 +348,7 @@ var Fieldset = { }, saveState: function (url, parameters) { new Ajax.Request(url, { - method: 'get', + method: 'post', parameters: Object.toQueryString(parameters), loaderArea: false }); diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js index fee0104efe87a..23f24bfcd1a9b 100755 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/setup.js @@ -238,10 +238,18 @@ define([ * @param {Object} o */ openFileBrowser: function (o) { - var typeTitle, - storeId = this.config['store_id'] !== null ? this.config['store_id'] : 0, - frameDialog = jQuery(o.win.frameElement).parents('[role="dialog"]'), - wUrl = this.config['files_browser_window_url'] + + var targetElementID = tinyMCE.activeEditor.getElement().getAttribute('id'), + originId = this.id, + typeTitle, + storeId, + frameDialog, + wUrl; + + this.initialize(targetElementID, this.config); + + storeId = this.config['store_id'] !== null ? this.config['store_id'] : 0; + frameDialog = jQuery(o.win.frameElement).parents('[role="dialog"]'); + wUrl = this.config['files_browser_window_url'] + 'target_element_id/' + this.id + '/' + 'store/' + storeId + '/'; @@ -255,6 +263,8 @@ define([ typeTitle = this.translate('Insert File...'); } + this.initialize(originId, this.config); + frameDialog.hide(); jQuery('#mceModalBlocker').hide(); diff --git a/lib/web/mage/gallery/gallery.js b/lib/web/mage/gallery/gallery.js index 15c3d01cf2be3..be78856b21fcd 100644 --- a/lib/web/mage/gallery/gallery.js +++ b/lib/web/mage/gallery/gallery.js @@ -141,7 +141,7 @@ define([ this.setupBreakpoints(); this.initFullscreenSettings(); - this.settings.$element.on('mouseup', '.fotorama__stage__frame', function () { + this.settings.$element.on('click', '.fotorama__stage__frame', function () { if ( !$(this).parents('.fotorama__shadows--left, .fotorama__shadows--right').length && !$(this).hasClass('fotorama-video-container') diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index a742b8e6bbb27..8c19669699b9d 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -1425,10 +1425,14 @@ ], 'validate-per-page-value-list': [ function (v) { - var isValid = !$.mage.isEmpty(v), + var isValid = true, values = v.split(','), i; + if ($.mage.isEmpty(v)) { + return isValid; + } + for (i = 0; i < values.length; i++) { if (!/^[0-9]+$/.test(values[i])) { isValid = false; @@ -1944,7 +1948,7 @@ } if (firstActive.length) { - $('html, body').animate({ + $('html, body').stop().animate({ scrollTop: firstActive.offset().top }); firstActive.focus(); diff --git a/lib/web/tiny_mce/themes/advanced/js/source_editor.js b/lib/web/tiny_mce/themes/advanced/js/source_editor.js index 9cf6b1a29cdaf..e90ee4d99628d 100644 --- a/lib/web/tiny_mce/themes/advanced/js/source_editor.js +++ b/lib/web/tiny_mce/themes/advanced/js/source_editor.js @@ -10,8 +10,9 @@ function onLoadInit() { tinyMCEPopup.resizeToInnerSize(); // Remove Gecko spellchecking - if (tinymce.isGecko) - document.body.spellcheck = tinyMCEPopup.editor.getParam("gecko_spellcheck"); + if (tinymce.isGecko) { + document.body.spellcheck = tinyMCEPopup.editor.getParam("gecko_spellcheck", false); + } document.getElementById('htmlSource').value = tinyMCEPopup.editor.getContent({source_view : true}); diff --git a/nginx.conf.sample b/nginx.conf.sample index 90604808f6ec0..2104a920258f8 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -33,6 +33,11 @@ charset UTF-8; error_page 404 403 = /errors/404.php; #add_header "X-UA-Compatible" "IE=Edge"; +# Deny access to sensitive files +location /.user.ini { + deny all; +} + # PHP entry point for setup application location ~* ^/setup($|/) { root $MAGE_ROOT; @@ -159,6 +164,11 @@ location /media/downloadable/ { location /media/import/ { deny all; } +location /errors/ { + location ~* \.xml$ { + deny all; + } +} # PHP entry point for main application location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check)\.php$ { @@ -198,6 +208,6 @@ gzip_types gzip_vary on; # Banned locations (only reached if the earlier PHP entry point regexes don't match) -location ~* (\.php$|\.htaccess$|\.git) { +location ~* (\.php$|\.phtml$|\.htaccess$|\.git) { deny all; } diff --git a/pub/.htaccess b/pub/.htaccess index 8ba04ff4415f3..9f07f3319837e 100644 --- a/pub/.htaccess +++ b/pub/.htaccess @@ -220,6 +220,16 @@ ErrorDocument 403 /errors/404.php Require all denied +## Deny access to .user.ini## + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + ############################################ diff --git a/pub/errors/.htaccess b/pub/errors/.htaccess index 3692dd439e2ff..a7b9cbda05893 100644 --- a/pub/errors/.htaccess +++ b/pub/errors/.htaccess @@ -1,4 +1,7 @@ Options None + + Deny from all + RewriteEngine Off diff --git a/pub/media/.htaccess b/pub/media/.htaccess index 28e65b490fbb8..d8793a891430a 100644 --- a/pub/media/.htaccess +++ b/pub/media/.htaccess @@ -23,6 +23,9 @@ SetHandler default-handler Options +FollowSymLinks RewriteEngine on + ## you can put here your pub/media folder path relative to web root + #RewriteBase /magento/pub/media/ + ############################################ ## never rewrite for existing files RewriteCond %{REQUEST_FILENAME} !-f diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index 64ff3bc9cba60..73403e543e918 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -32789,7 +32789,15 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate - + + + true + ${admin_form_key} + = + true + form_key + + @@ -32798,7 +32806,7 @@ vars.put("new_parent_category_id", props.get("admin_category_ids_list").get(cate ${request_protocol} ${base_path}${admin_path}/catalog/category/delete/id/${admin_category_id}/ - GET + POST true false true diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList.php b/setup/src/Magento/Setup/Model/ConfigOptionsList.php index fa79139e73313..29a868b1f0eb2 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList.php @@ -44,7 +44,8 @@ class ConfigOptionsList implements ConfigOptionsListInterface private $configOptionsListClasses = [ \Magento\Setup\Model\ConfigOptionsList\Session::class, \Magento\Setup\Model\ConfigOptionsList\Cache::class, - \Magento\Setup\Model\ConfigOptionsList\PageCache::class + \Magento\Setup\Model\ConfigOptionsList\PageCache::class, + \Magento\Setup\Model\ConfigOptionsList\Lock::class, ]; /** diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php index 04ec83a3d0ca2..8bdfd2b0a91a5 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Cache.php @@ -27,12 +27,14 @@ class Cache implements ConfigOptionsListInterface const INPUT_KEY_CACHE_BACKEND_REDIS_DATABASE = 'cache-backend-redis-db'; const INPUT_KEY_CACHE_BACKEND_REDIS_PORT = 'cache-backend-redis-port'; const INPUT_KEY_CACHE_BACKEND_REDIS_PASSWORD = 'cache-backend-redis-password'; + const INPUT_KEY_CACHE_ID_PREFIX = 'cache-id-prefix'; const CONFIG_PATH_CACHE_BACKEND = 'cache/frontend/default/backend'; const CONFIG_PATH_CACHE_BACKEND_SERVER = 'cache/frontend/default/backend_options/server'; const CONFIG_PATH_CACHE_BACKEND_DATABASE = 'cache/frontend/default/backend_options/database'; const CONFIG_PATH_CACHE_BACKEND_PORT = 'cache/frontend/default/backend_options/port'; const CONFIG_PATH_CACHE_BACKEND_PASSWORD = 'cache/frontend/default/backend_options/password'; + const CONFIG_PATH_CACHE_ID_PREFIX = 'cache/frontend/default/id_prefix'; /** * @var array @@ -112,6 +114,12 @@ public function getOptions() TextConfigOption::FRONTEND_WIZARD_TEXT, self::CONFIG_PATH_CACHE_BACKEND_PASSWORD, 'Redis server password' + ), + new TextConfigOption( + self::INPUT_KEY_CACHE_ID_PREFIX, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_CACHE_ID_PREFIX, + 'ID prefix for cache keys' ) ]; } @@ -122,6 +130,11 @@ public function getOptions() public function createConfig(array $options, DeploymentConfig $deploymentConfig) { $configData = new ConfigData(ConfigFilePool::APP_ENV); + if (isset($options[self::INPUT_KEY_CACHE_ID_PREFIX])) { + $configData->set(self::CONFIG_PATH_CACHE_ID_PREFIX, $options[self::INPUT_KEY_CACHE_ID_PREFIX]); + } else { + $configData->set(self::CONFIG_PATH_CACHE_ID_PREFIX, $this->generateCachePrefix()); + } if (isset($options[self::INPUT_KEY_CACHE_BACKEND])) { if ($options[self::INPUT_KEY_CACHE_BACKEND] == self::INPUT_VALUE_CACHE_REDIS) { @@ -241,4 +254,14 @@ private function getDefaultConfigValue($inputKey) return ''; } } + + /** + * Generate default cache ID prefix based on installation dir + * + * @return string + */ + private function generateCachePrefix(): string + { + return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; + } } diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php new file mode 100644 index 0000000000000..66f41128c46b1 --- /dev/null +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Lock.php @@ -0,0 +1,342 @@ + [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + self::INPUT_KEY_LOCK_DB_PREFIX => self::CONFIG_PATH_LOCK_DB_PREFIX, + ], + LockBackendFactory::LOCK_ZOOKEEPER => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + self::INPUT_KEY_LOCK_ZOOKEEPER_HOST => self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, + self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, + ], + LockBackendFactory::LOCK_CACHE => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + ], + LockBackendFactory::LOCK_FILE => [ + self::INPUT_KEY_LOCK_PROVIDER => self::CONFIG_PATH_LOCK_PROVIDER, + self::INPUT_KEY_LOCK_FILE_PATH => self::CONFIG_PATH_LOCK_FILE_PATH, + ], + ]; + + /** + * The list of default values + * + * @var array + */ + private $defaultConfigValues = [ + self::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_DB, + self::INPUT_KEY_LOCK_DB_PREFIX => null, + self::INPUT_KEY_LOCK_ZOOKEEPER_PATH => ZookeeperLock::DEFAULT_PATH, + ]; + + /** + * @inheritdoc + */ + public function getOptions() + { + return [ + new SelectConfigOption( + self::INPUT_KEY_LOCK_PROVIDER, + SelectConfigOption::FRONTEND_WIZARD_SELECT, + $this->validLockProviders, + self::CONFIG_PATH_LOCK_PROVIDER, + 'Lock provider name', + LockBackendFactory::LOCK_DB + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_DB_PREFIX, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_DB_PREFIX, + 'Installation specific lock prefix to avoid lock conflicts' + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_ZOOKEEPER_HOST, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, + 'Host and port to connect to Zookeeper cluster. For example: 127.0.0.1:2181' + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_ZOOKEEPER_PATH, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, + 'The path where Zookeeper will save locks. The default path is: ' . ZookeeperLock::DEFAULT_PATH + ), + new TextConfigOption( + self::INPUT_KEY_LOCK_FILE_PATH, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_LOCK_FILE_PATH, + 'The path where file locks will be saved.' + ), + ]; + } + + /** + * @inheritdoc + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig) + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + $configData->setOverrideWhenSave(true); + $lockProvider = $this->getLockProvider($options, $deploymentConfig); + + $this->setDefaultConfiguration($configData, $deploymentConfig, $lockProvider); + + foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { + if (isset($options[$input])) { + $configData->set($path, $options[$input]); + } + } + + return $configData; + } + + /** + * @inheritdoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + $lockProvider = $this->getLockProvider($options, $deploymentConfig); + switch ($lockProvider) { + case LockBackendFactory::LOCK_ZOOKEEPER: + $errors = $this->validateZookeeperConfig($options, $deploymentConfig); + break; + case LockBackendFactory::LOCK_FILE: + $errors = $this->validateFileConfig($options, $deploymentConfig); + break; + case LockBackendFactory::LOCK_CACHE: + case LockBackendFactory::LOCK_DB: + $errors = []; + break; + default: + $errors[] = 'The lock provider ' . $lockProvider . ' does not exist.'; + } + + return $errors; + } + + /** + * Validates File locks configuration + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return array + */ + private function validateFileConfig(array $options, DeploymentConfig $deploymentConfig): array + { + $errors = []; + + $path = $options[self::INPUT_KEY_LOCK_FILE_PATH] + ?? $deploymentConfig->get( + self::CONFIG_PATH_LOCK_FILE_PATH, + $this->getDefaultValue(self::INPUT_KEY_LOCK_FILE_PATH) + ); + + if (!$path) { + $errors[] = 'The path needs to be a non-empty string.'; + } + + return $errors; + } + + /** + * Validates Zookeeper configuration + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return array + */ + private function validateZookeeperConfig(array $options, DeploymentConfig $deploymentConfig): array + { + $errors = []; + + if (!extension_loaded(LockBackendFactory::LOCK_ZOOKEEPER)) { + $errors[] = 'php extension Zookeeper is not installed.'; + } + + $host = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_HOST] + ?? $deploymentConfig->get( + self::CONFIG_PATH_LOCK_ZOOKEEPER_HOST, + $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_HOST) + ); + $path = $options[self::INPUT_KEY_LOCK_ZOOKEEPER_PATH] + ?? $deploymentConfig->get( + self::CONFIG_PATH_LOCK_ZOOKEEPER_PATH, + $this->getDefaultValue(self::INPUT_KEY_LOCK_ZOOKEEPER_PATH) + ); + + if (!$path) { + $errors[] = 'Zookeeper path needs to be a non-empty string.'; + } + + if (!$host) { + $errors[] = 'Zookeeper host is should be set.'; + } + + return $errors; + } + + /** + * Returns the name of lock provider + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return string + */ + private function getLockProvider(array $options, DeploymentConfig $deploymentConfig): string + { + if (!isset($options[self::INPUT_KEY_LOCK_PROVIDER])) { + return (string) $deploymentConfig->get( + self::CONFIG_PATH_LOCK_PROVIDER, + $this->getDefaultValue(self::INPUT_KEY_LOCK_PROVIDER) + ); + } + + return (string) $options[self::INPUT_KEY_LOCK_PROVIDER]; + } + + /** + * Sets default configuration for locks + * + * @param ConfigData $configData + * @param DeploymentConfig $deploymentConfig + * @param string $lockProvider + * @return ConfigData + */ + private function setDefaultConfiguration( + ConfigData $configData, + DeploymentConfig $deploymentConfig, + string $lockProvider + ) { + foreach ($this->mappingInputKeyToConfigPath[$lockProvider] as $input => $path) { + $configData->set($path, $deploymentConfig->get($path, $this->getDefaultValue($input))); + } + + return $configData; + } + + /** + * Returns default value by input key + * + * If default value is not set returns null + * + * @param string $inputKey + * @return mixed|null + */ + private function getDefaultValue(string $inputKey) + { + if (isset($this->defaultConfigValues[$inputKey])) { + return $this->defaultConfigValues[$inputKey]; + } else { + return null; + } + } +} diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php index 944c543495751..a0dd19034621b 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/PageCache.php @@ -28,6 +28,7 @@ class PageCache implements ConfigOptionsListInterface const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PORT = 'page-cache-redis-port'; const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_COMPRESS_DATA = 'page-cache-redis-compress-data'; const INPUT_KEY_PAGE_CACHE_BACKEND_REDIS_PASSWORD = 'page-cache-redis-password'; + const INPUT_KEY_PAGE_CACHE_ID_PREFIX = 'page-cache-id-prefix'; const CONFIG_PATH_PAGE_CACHE_BACKEND = 'cache/frontend/page_cache/backend'; const CONFIG_PATH_PAGE_CACHE_BACKEND_SERVER = 'cache/frontend/page_cache/backend_options/server'; @@ -35,6 +36,7 @@ class PageCache implements ConfigOptionsListInterface const CONFIG_PATH_PAGE_CACHE_BACKEND_PORT = 'cache/frontend/page_cache/backend_options/port'; const CONFIG_PATH_PAGE_CACHE_BACKEND_COMPRESS_DATA = 'cache/frontend/page_cache/backend_options/compress_data'; const CONFIG_PATH_PAGE_CACHE_BACKEND_PASSWORD = 'cache/frontend/page_cache/backend_options/password'; + const CONFIG_PATH_PAGE_CACHE_ID_PREFIX = 'cache/frontend/page_cache/id_prefix'; /** * @var array @@ -122,6 +124,12 @@ public function getOptions() TextConfigOption::FRONTEND_WIZARD_TEXT, self::CONFIG_PATH_PAGE_CACHE_BACKEND_PASSWORD, 'Redis server password' + ), + new TextConfigOption( + self::INPUT_KEY_PAGE_CACHE_ID_PREFIX, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_PAGE_CACHE_ID_PREFIX, + 'ID prefix for cache keys' ) ]; } @@ -132,6 +140,11 @@ public function getOptions() public function createConfig(array $options, DeploymentConfig $deploymentConfig) { $configData = new ConfigData(ConfigFilePool::APP_ENV); + if (isset($options[self::INPUT_KEY_PAGE_CACHE_ID_PREFIX])) { + $configData->set(self::CONFIG_PATH_PAGE_CACHE_ID_PREFIX, $options[self::INPUT_KEY_PAGE_CACHE_ID_PREFIX]); + } else { + $configData->set(self::CONFIG_PATH_PAGE_CACHE_ID_PREFIX, $this->generateCachePrefix()); + } if (isset($options[self::INPUT_KEY_PAGE_CACHE_BACKEND])) { if ($options[self::INPUT_KEY_PAGE_CACHE_BACKEND] == self::INPUT_VALUE_PAGE_CACHE_REDIS) { @@ -252,4 +265,14 @@ private function getDefaultConfigValue($inputKey) return ''; } } + + /** + * Generate default cache ID prefix based on installation dir + * + * @return string + */ + private function generateCachePrefix(): string + { + return substr(\md5(dirname(__DIR__, 6)), 0, 3) . '_'; + } } diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/CacheTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/CacheTest.php index 39b95953c6347..f351bca65f89b 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/CacheTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/CacheTest.php @@ -28,6 +28,9 @@ class CacheTest extends \PHPUnit\Framework\TestCase */ private $deploymentConfigMock; + /** + * @inheritdoc + */ protected function setUp() { $this->validatorMock = $this->createMock(RedisConnectionValidator::class); @@ -39,7 +42,7 @@ protected function setUp() public function testGetOptions() { $options = $this->configOptionsList->getOptions(); - $this->assertCount(5, $options); + $this->assertCount(6, $options); $this->assertArrayHasKey(0, $options); $this->assertInstanceOf(SelectConfigOption::class, $options[0]); @@ -60,6 +63,10 @@ public function testGetOptions() $this->assertArrayHasKey(4, $options); $this->assertInstanceOf(TextConfigOption::class, $options[4]); $this->assertEquals('cache-backend-redis-password', $options[4]->getName()); + + $this->assertArrayHasKey(5, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[5]); + $this->assertEquals('cache-id-prefix', $options[5]->getName()); } public function testCreateConfigCacheRedis() @@ -76,7 +83,8 @@ public function testCreateConfigCacheRedis() 'port' => '', 'database' => '', 'password' => '' - ] + ], + 'id_prefix' => $this->expectedIdPrefix(), ] ] ] @@ -99,7 +107,8 @@ public function testCreateConfigWithRedisConfig() 'port' => '1234', 'database' => '5', 'password' => '' - ] + ], + 'id_prefix' => $this->expectedIdPrefix(), ] ] ] @@ -116,6 +125,48 @@ public function testCreateConfigWithRedisConfig() $this->assertEquals($expectedConfigData, $configData->getData()); } + public function testCreateConfigWithFileCache() + { + $this->deploymentConfigMock->method('get')->willReturn(''); + + $expectedConfigData = [ + 'cache' => [ + 'frontend' => [ + 'default' => [ + 'id_prefix' => $this->expectedIdPrefix(), + ] + ] + ] + ]; + + $configData = $this->configOptionsList->createConfig([], $this->deploymentConfigMock); + + $this->assertEquals($expectedConfigData, $configData->getData()); + } + + public function testCreateConfigWithIdPrefix() + { + $this->deploymentConfigMock->method('get')->willReturn(''); + + $explicitPrefix = 'XXX_'; + $expectedConfigData = [ + 'cache' => [ + 'frontend' => [ + 'default' => [ + 'id_prefix' => $explicitPrefix, + ] + ] + ] + ]; + + $configData = $this->configOptionsList->createConfig( + ['cache-id-prefix' => $explicitPrefix], + $this->deploymentConfigMock + ); + + $this->assertEquals($expectedConfigData, $configData->getData()); + } + public function testValidateWithValidInput() { $options = [ @@ -142,4 +193,14 @@ public function testValidateWithInvalidInput() $this->assertCount(1, $errors); $this->assertEquals("Invalid cache handler 'clay-tablet'", $errors[0]); } + + /** + * The default ID prefix, based on installation directory + * + * @return string + */ + private function expectedIdPrefix(): string + { + return substr(\md5(dirname(__DIR__, 8)), 0, 3) . '_'; + } } diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/LockTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/LockTest.php new file mode 100644 index 0000000000000..1a46bddf5f21a --- /dev/null +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/LockTest.php @@ -0,0 +1,232 @@ +deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->lockConfigOptionsList = new LockConfigOptionsList(); + } + + /** + * @return void + */ + public function testGetOptions() + { + $options = $this->lockConfigOptionsList->getOptions(); + $this->assertSame(5, count($options)); + + $this->assertArrayHasKey(0, $options); + $this->assertInstanceOf(SelectConfigOption::class, $options[0]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER, $options[0]->getName()); + + $this->assertArrayHasKey(1, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[1]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_DB_PREFIX, $options[1]->getName()); + + $this->assertArrayHasKey(2, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[2]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_HOST, $options[2]->getName()); + + $this->assertArrayHasKey(3, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[3]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_PATH, $options[3]->getName()); + + $this->assertArrayHasKey(4, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[4]); + $this->assertEquals(LockConfigOptionsList::INPUT_KEY_LOCK_FILE_PATH, $options[4]->getName()); + } + + /** + * @param array $options + * @param array $expectedResult + * @dataProvider createConfigDataProvider + */ + public function testCreateConfig(array $options, array $expectedResult) + { + $this->deploymentConfigMock->expects($this->any()) + ->method('get') + ->willReturnArgument(1); + $data = $this->lockConfigOptionsList->createConfig($options, $this->deploymentConfigMock); + $this->assertInstanceOf(ConfigData::class, $data); + $this->assertTrue($data->isOverrideWhenSave()); + $this->assertSame($expectedResult, $data->getData()); + } + + /** + * @return array + */ + public function createConfigDataProvider(): array + { + return [ + 'Check default values' => [ + 'options' => [], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_DB, + 'config' => [ + 'prefix' => null, + ], + ], + ], + ], + 'Check default value for cache lock' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_CACHE, + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_CACHE, + ], + ], + ], + 'Check default value for zookeeper lock' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_ZOOKEEPER, + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'config' => [ + 'host' => null, + 'path' => ZookeeperLock::DEFAULT_PATH, + ], + ], + ], + ], + 'Check specific db lock options' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_DB, + LockConfigOptionsList::INPUT_KEY_LOCK_DB_PREFIX => 'my_prefix' + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_DB, + 'config' => [ + 'prefix' => 'my_prefix', + ], + ], + ], + ], + 'Check specific zookeeper lock options' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_ZOOKEEPER, + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_HOST => '123.45.67.89:10', + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_PATH => '/some/path', + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'config' => [ + 'host' => '123.45.67.89:10', + 'path' => '/some/path', + ], + ], + ], + ], + 'Check specific file lock options' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_FILE, + LockConfigOptionsList::INPUT_KEY_LOCK_FILE_PATH => '/my/path' + ], + 'expectedResult' => [ + 'lock' => [ + 'provider' => LockBackendFactory::LOCK_FILE, + 'config' => [ + 'path' => '/my/path', + ], + ], + ], + ], + ]; + } + + /** + * @param array $options + * @param array $expectedResult + * @dataProvider validateDataProvider + */ + public function testValidate(array $options, array $expectedResult) + { + $this->deploymentConfigMock->expects($this->any()) + ->method('get') + ->willReturnArgument(1); + $this->assertSame( + $expectedResult, + $this->lockConfigOptionsList->validate($options, $this->deploymentConfigMock) + ); + } + + /** + * @return array + */ + public function validateDataProvider(): array + { + return [ + 'Wrong lock provider' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => 'SomeProvider', + ], + 'expectedResult' => [ + 'The lock provider SomeProvider does not exist.', + ], + ], + 'Empty host and path for Zookeeper' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_ZOOKEEPER, + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_HOST => '', + LockConfigOptionsList::INPUT_KEY_LOCK_ZOOKEEPER_PATH => '', + ], + 'expectedResult' => extension_loaded('zookeeper') + ? [ + 'Zookeeper path needs to be a non-empty string.', + 'Zookeeper host is should be set.', + ] + : [ + 'php extension Zookeeper is not installed.', + 'Zookeeper path needs to be a non-empty string.', + 'Zookeeper host is should be set.', + ], + ], + 'Empty path for File lock' => [ + 'options' => [ + LockConfigOptionsList::INPUT_KEY_LOCK_PROVIDER => LockBackendFactory::LOCK_FILE, + LockConfigOptionsList::INPUT_KEY_LOCK_FILE_PATH => '', + ], + 'expectedResult' => [ + 'The path needs to be a non-empty string.', + ], + ], + ]; + } +} diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/PageCacheTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/PageCacheTest.php index ed0e567820ad1..0e7c851cb706b 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/PageCacheTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsList/PageCacheTest.php @@ -28,6 +28,9 @@ class PageCacheTest extends \PHPUnit\Framework\TestCase */ private $deploymentConfigMock; + /** + * @inheritdoc + */ protected function setUp() { $this->validatorMock = $this->createMock(RedisConnectionValidator::class, [], [], '', false); @@ -39,7 +42,7 @@ protected function setUp() public function testGetOptions() { $options = $this->configList->getOptions(); - $this->assertCount(6, $options); + $this->assertCount(7, $options); $this->assertArrayHasKey(0, $options); $this->assertInstanceOf(SelectConfigOption::class, $options[0]); @@ -64,6 +67,10 @@ public function testGetOptions() $this->assertArrayHasKey(5, $options); $this->assertInstanceOf(TextConfigOption::class, $options[5]); $this->assertEquals('page-cache-redis-password', $options[5]->getName()); + + $this->assertArrayHasKey(6, $options); + $this->assertInstanceOf(TextConfigOption::class, $options[6]); + $this->assertEquals('page-cache-id-prefix', $options[6]->getName()); } public function testCreateConfigWithRedis() @@ -81,7 +88,8 @@ public function testCreateConfigWithRedis() 'database' => '', 'compress_data' => '', 'password' => '' - ] + ], + 'id_prefix' => $this->expectedIdPrefix(), ] ] ] @@ -105,7 +113,8 @@ public function testCreateConfigWithRedisConfiguration() 'database' => '6', 'compress_data' => '1', 'password' => '' - ] + ], + 'id_prefix' => $this->expectedIdPrefix(), ] ] ] @@ -124,6 +133,48 @@ public function testCreateConfigWithRedisConfiguration() $this->assertEquals($expectedConfigData, $configData->getData()); } + public function testCreateConfigWithFileCache() + { + $this->deploymentConfigMock->method('get')->willReturn(''); + + $expectedConfigData = [ + 'cache' => [ + 'frontend' => [ + 'page_cache' => [ + 'id_prefix' => $this->expectedIdPrefix(), + ] + ] + ] + ]; + + $configData = $this->configList->createConfig([], $this->deploymentConfigMock); + + $this->assertEquals($expectedConfigData, $configData->getData()); + } + + public function testCreateConfigWithIdPrefix() + { + $this->deploymentConfigMock->method('get')->willReturn(''); + + $explicitPrefix = 'XXX_'; + $expectedConfigData = [ + 'cache' => [ + 'frontend' => [ + 'page_cache' => [ + 'id_prefix' => $explicitPrefix, + ] + ] + ] + ]; + + $configData = $this->configList->createConfig( + ['page-cache-id-prefix' => $explicitPrefix], + $this->deploymentConfigMock + ); + + $this->assertEquals($expectedConfigData, $configData->getData()); + } + public function testValidationWithValidData() { $this->validatorMock->expects($this->once()) @@ -151,4 +202,14 @@ public function testValidationWithInvalidData() $this->assertCount(1, $errors); $this->assertEquals('Invalid cache handler \'foobar\'', $errors[0]); } + + /** + * The default ID prefix, based on installation directory + * + * @return string + */ + private function expectedIdPrefix(): string + { + return substr(\md5(dirname(__DIR__, 8)), 0, 3) . '_'; + } } diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php index f342a11493498..99ef82dd9d355 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ConfigOptionsListTest.php @@ -7,6 +7,7 @@ namespace Magento\Setup\Test\Unit\Model; use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Setup\Model\ConfigOptionsList\Lock; use Magento\Setup\Model\ConfigGenerator; use Magento\Setup\Model\ConfigOptionsList; use Magento\Setup\Validator\DbValidator; @@ -82,7 +83,7 @@ public function testCreateOptions() $this->generator->expects($this->once())->method('createXFrameConfig')->willReturn($configDataMock); $this->generator->expects($this->once())->method('createCacheHostsConfig')->willReturn($configDataMock); - $configData = $this->object->createConfig([], $this->deploymentConfig); + $configData = $this->object->createConfig([Lock::INPUT_KEY_LOCK_PROVIDER => 'db'], $this->deploymentConfig); $this->assertGreaterThanOrEqual(6, count($configData)); } @@ -96,7 +97,7 @@ public function testCreateOptionsWithOptionalNull() $this->generator->expects($this->once())->method('createXFrameConfig')->willReturn($configDataMock); $this->generator->expects($this->once())->method('createCacheHostsConfig')->willReturn($configDataMock); - $configData = $this->object->createConfig([], $this->deploymentConfig); + $configData = $this->object->createConfig([Lock::INPUT_KEY_LOCK_PROVIDER => 'db'], $this->deploymentConfig); $this->assertGreaterThanOrEqual(6, count($configData)); } @@ -109,7 +110,8 @@ public function testValidateSuccess() ConfigOptionsListConstants::INPUT_KEY_DB_NAME => 'name', ConfigOptionsListConstants::INPUT_KEY_DB_HOST => 'host', ConfigOptionsListConstants::INPUT_KEY_DB_USER => 'user', - ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass' + ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass', + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $this->prepareValidationMocks(); @@ -127,7 +129,8 @@ public function testValidateInvalidSessionHandler() ConfigOptionsListConstants::INPUT_KEY_DB_NAME => 'name', ConfigOptionsListConstants::INPUT_KEY_DB_HOST => 'host', ConfigOptionsListConstants::INPUT_KEY_DB_USER => 'user', - ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass' + ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD => 'pass', + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $this->prepareValidationMocks(); @@ -141,7 +144,8 @@ public function testValidateEmptyEncryptionKey() { $options = [ ConfigOptionsListConstants::INPUT_KEY_SKIP_DB_VALIDATION => true, - ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY => '' + ConfigOptionsListConstants::INPUT_KEY_ENCRYPTION_KEY => '', + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $this->assertEquals( ['Invalid encryption key'], @@ -167,7 +171,8 @@ public function testValidateCacheHosts($hosts, $expectedError) { $options = [ ConfigOptionsListConstants::INPUT_KEY_SKIP_DB_VALIDATION => true, - ConfigOptionsListConstants::INPUT_KEY_CACHE_HOSTS => $hosts + ConfigOptionsListConstants::INPUT_KEY_CACHE_HOSTS => $hosts, + Lock::INPUT_KEY_LOCK_PROVIDER => 'db' ]; $result = $this->object->validate($options, $this->deploymentConfig); if ($expectedError) { diff --git a/setup/view/styles/lib/variables/_colors.less b/setup/view/styles/lib/variables/_colors.less index 638490ac8673a..a72dc69ac7669 100644 --- a/setup/view/styles/lib/variables/_colors.less +++ b/setup/view/styles/lib/variables/_colors.less @@ -24,7 +24,7 @@ @color-green-apple: #79a22e; @color-green-islamic: #090; @color-dark-brownie: #41362f; -@color-brown-darkie: #41362f; +@color-brown-darker: #41362f; @color-phoenix-down: #e04f00; @color-phoenix: #eb5202; @color-phoenix-almost-rise: #ef672f;