diff --git a/CHANGELOG.md b/CHANGELOG.md index 0999bee6ea415..04fb46a825f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,619 @@ +2.3.2 +============= +* GitHub issues: + * [#19596](https://github.com/magento/magento2/issues/19596) -- Images in XML sitemap are always linked to base store in multistore on Schedule (fixed in [magento/magento2#19598](https://github.com/magento/magento2/pull/19598)) + * [#20010](https://github.com/magento/magento2/issues/20010) -- Wrong price amount in opengraph (fixed in [magento/magento2#20011](https://github.com/magento/magento2/pull/20011)) + * [#20091](https://github.com/magento/magento2/issues/20091) -- Error when uploading a Transactional Emails logo (fixed in [magento/magento2#20092](https://github.com/magento/magento2/pull/20092)) + * [#20172](https://github.com/magento/magento2/issues/20172) -- On customer login page input field are short width on tablet view (fixed in [magento/magento2#20173](https://github.com/magento/magento2/pull/20173)) + * [#20380](https://github.com/magento/magento2/issues/20380) -- Get Shipping Method as object from order instance gives undefined index. (fixed in [magento/magento2#20381](https://github.com/magento/magento2/pull/20381)) + * [#20376](https://github.com/magento/magento2/issues/20376) -- Image gets uploaded if field is disable in Category (fixed in [magento/magento2#20461](https://github.com/magento/magento2/pull/20461)) + * [#20555](https://github.com/magento/magento2/issues/20555) -- Meta Keywords/Meta Description are input field in product form while they are defined as textarea (fixed in [magento/magento2#20556](https://github.com/magento/magento2/pull/20556)) + * [#19630](https://github.com/magento/magento2/issues/19630) -- Checkbox alignment issue backend. (fixed in [magento/magento2#19631](https://github.com/magento/magento2/pull/19631)) + * [#19891](https://github.com/magento/magento2/issues/19891) -- product_type attribute contains incorrect value in mass import export csv after creating custom type_id attribute. actual type_id value in database gets change with newly created attribute type_id. (fixed in [magento/magento2#20115](https://github.com/magento/magento2/pull/20115)) + * [#21021](https://github.com/magento/magento2/issues/21021) -- products in category checkbox not align properly (fixed in [magento/magento2#21022](https://github.com/magento/magento2/pull/21022)) + * [#21089](https://github.com/magento/magento2/issues/21089) -- No accessible label for vault-saved credit card type (fixed in [magento/magento2#21090](https://github.com/magento/magento2/pull/21090)) + * [#263](https://github.com/magento/magento2/issues/263) -- Where Mage_Core_Model_Config::applyClassRewrites($className) method is (fixed in [magento/graphql-ce#347](https://github.com/magento/graphql-ce/pull/347)) + * [#20163](https://github.com/magento/magento2/issues/20163) -- On iPhone5 device newsletter subscription input box not contain complete text (placeholder) (fixed in [magento/magento2#20165](https://github.com/magento/magento2/pull/20165)) + * [#20299](https://github.com/magento/magento2/issues/20299) -- Order item details label not aligned in mobile view (fixed in [magento/magento2#20466](https://github.com/magento/magento2/pull/20466)) + * [#11358](https://github.com/magento/magento2/issues/11358) -- Full Tax Summary display wrong numbers. (fixed in [magento/magento2#20682](https://github.com/magento/magento2/pull/20682)) + * [#19701](https://github.com/magento/magento2/issues/19701) -- Magento 2.3 Shopping Cart Taxes Missing Calc Line (fixed in [magento/magento2#20682](https://github.com/magento/magento2/pull/20682)) + * [#17861](https://github.com/magento/magento2/issues/17861) -- Customer Name Prefix shows white space when extra separator is addes. (fixed in [magento/magento2#20896](https://github.com/magento/magento2/pull/20896)) + * [#20888](https://github.com/magento/magento2/issues/20888) -- Entered data missing when entering the wrong date for from, to in cart rule (fixed in [magento/magento2#20895](https://github.com/magento/magento2/pull/20895)) + * [#17564](https://github.com/magento/magento2/issues/17564) -- Magento 2 inline edit date issues in admin grid with Ui Component (fixed in [magento/magento2#20902](https://github.com/magento/magento2/pull/20902)) + * [#18698](https://github.com/magento/magento2/issues/18698) -- Magento triggers and sends some of order emails exactly one month later,while the order email was not enabled then (fixed in [magento/magento2#20953](https://github.com/magento/magento2/pull/20953)) + * [#20919](https://github.com/magento/magento2/issues/20919) -- Email label and email field not aligned from left for reorder of guest user (fixed in [magento/magento2#21009](https://github.com/magento/magento2/pull/21009)) + * [#21070](https://github.com/magento/magento2/issues/21070) -- Luma theme my account Order Information status tabs break in tablet view (fixed in [magento/magento2#21071](https://github.com/magento/magento2/pull/21071)) + * [#21101](https://github.com/magento/magento2/issues/21101) -- Unable to open the product from sidebar's Compare Products block (fixed in [magento/magento2#21102](https://github.com/magento/magento2/pull/21102)) + * [#6162](https://github.com/magento/magento2/issues/6162) -- Can't set customer group when creating a new order in the admin. (fixed in [magento/magento2#21145](https://github.com/magento/magento2/pull/21145)) + * [#7974](https://github.com/magento/magento2/issues/7974) -- Can't change customer group when placing an admin order, even after MAGETWO-57077 applied (fixed in [magento/magento2#21145](https://github.com/magento/magento2/pull/21145)) + * [#21144](https://github.com/magento/magento2/issues/21144) -- Can't change customer group when placing an admin order (fixed in [magento/magento2#21145](https://github.com/magento/magento2/pull/21145)) + * [#18056](https://github.com/magento/magento2/issues/18056) -- CacheInvalidate : stop at first server not responding (fixed in [magento/magento2#18852](https://github.com/magento/magento2/pull/18852)) + * [#19561](https://github.com/magento/magento2/issues/19561) -- Custom option price calculation is wrong with multi currency when option price type is percentage. (fixed in [magento/magento2#19608](https://github.com/magento/magento2/pull/19608)) + * [#18944](https://github.com/magento/magento2/issues/18944) -- Unable to open URL for downloadable product in 2.2.6 (fixed in [magento/magento2#19996](https://github.com/magento/magento2/pull/19996)) + * [#18347](https://github.com/magento/magento2/issues/18347) -- Element 'css', attribute 'as': The attribute 'as' is not allowed. (CSS preloading) (fixed in [magento/magento2#20495](https://github.com/magento/magento2/pull/20495)) + * [#19328](https://github.com/magento/magento2/issues/19328) -- Success Message Icon vertically misaligned in admin panel (fixed in [magento/magento2#21069](https://github.com/magento/magento2/pull/21069)) + * [#19274](https://github.com/magento/magento2/issues/19274) -- Why is SessionManager used instead of its Interface? (fixed in [magento/magento2#19359](https://github.com/magento/magento2/pull/19359)) + * [#19166](https://github.com/magento/magento2/issues/19166) -- Customer related values are NULL for guests converted to customers after checkout. (fixed in [magento/magento2#19191](https://github.com/magento/magento2/pull/19191)) + * [#19485](https://github.com/magento/magento2/issues/19485) -- DHL Shipping Quotes fail for Domestic Shipments when Content Mode is "Non Documents" (fixed in [magento/magento2#19487](https://github.com/magento/magento2/pull/19487)) + * [#20838](https://github.com/magento/magento2/issues/20838) -- Luma theme shipping tool tip icon cut from leftside (fixed in [magento/magento2#20839](https://github.com/magento/magento2/pull/20839)) + * [#21196](https://github.com/magento/magento2/issues/21196) -- [UI] The dropdown state doesn't change if the dropdown is expanded or not (fixed in [magento/magento2#21197](https://github.com/magento/magento2/pull/21197)) + * [#5021](https://github.com/magento/magento2/issues/5021) -- "Please specify a shipping method" Exception (fixed in [magento/magento2#19505](https://github.com/magento/magento2/pull/19505)) + * [#21177](https://github.com/magento/magento2/issues/21177) -- Cart Page cross sell product Add to cart button overlapping (fixed in [magento/magento2#21178](https://github.com/magento/magento2/pull/21178)) + * [#20969](https://github.com/magento/magento2/issues/20969) -- Poor performance for some layered navigation queries (fixed in [magento/magento2#20971](https://github.com/magento/magento2/pull/20971)) + * [#14882](https://github.com/magento/magento2/issues/14882) -- product_types.xml doesn't allow numbers in modelInstance (fixed in [magento/magento2#20617](https://github.com/magento/magento2/pull/20617)) + * [#21271](https://github.com/magento/magento2/issues/21271) -- Address book display horizontal scroll in responsive view (fixed in [magento/magento2#21272](https://github.com/magento/magento2/pull/21272)) + * [#292](https://github.com/magento/magento2/issues/292) -- Refactor Mage_Rating_Model_Resource_Rating_Collection (fixed in [magento/graphql-ce#327](https://github.com/magento/graphql-ce/pull/327)) + * [#239](https://github.com/magento/magento2/issues/239) -- Feature Request: Record User Agent In Admin Grid (fixed in [magento/graphql-ce#364](https://github.com/magento/graphql-ce/pull/364)) + * [#17784](https://github.com/magento/magento2/issues/17784) -- store_view_code column has empty values in csv (fixed in [magento/magento2#19395](https://github.com/magento/magento2/pull/19395)) + * [#19786](https://github.com/magento/magento2/issues/19786) -- Can only export default store view items when upgrade to 2.3.0 (fixed in [magento/magento2#19395](https://github.com/magento/magento2/pull/19395)) + * [#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#20071](https://github.com/magento/magento2/pull/20071)) + * [#20855](https://github.com/magento/magento2/issues/20855) -- In checkout page product price not align proper in order summary block for ipad view (fixed in [magento/magento2#20856](https://github.com/magento/magento2/pull/20856)) + * [#21296](https://github.com/magento/magento2/issues/21296) -- Pagination drop-down size does not appropriate. (fixed in [magento/magento2#21298](https://github.com/magento/magento2/pull/21298)) + * [#8086](https://github.com/magento/magento2/issues/8086) -- Multiline admin field is broken (fixed in [magento/magento2#20371](https://github.com/magento/magento2/pull/20371)) + * [#18115](https://github.com/magento/magento2/issues/18115) -- Multiline field is broken (fixed in [magento/magento2#20371](https://github.com/magento/magento2/pull/20371)) + * [#8479](https://github.com/magento/magento2/issues/8479) -- Sequence of module load order should be deterministic (fixed in [magento/magento2#21020](https://github.com/magento/magento2/pull/21020)) + * [#16116](https://github.com/magento/magento2/issues/16116) -- Modules sort order in config.php is being inconsistent when no changes being made (fixed in [magento/magento2#21020](https://github.com/magento/magento2/pull/21020)) + * [#14412](https://github.com/magento/magento2/issues/14412) -- Magento 2.2.3 TypeErrors Cannot read property 'quoteData' / 'storecode' / 'sectionLoadUrl' of undefined (fixed in [magento/magento2#18503](https://github.com/magento/magento2/pull/18503)) + * [#19983](https://github.com/magento/magento2/issues/19983) -- Can't work customer Image attribute programmatically (fixed in [magento/magento2#19988](https://github.com/magento/magento2/pull/19988)) + * [#20305](https://github.com/magento/magento2/issues/20305) -- Update button on payment checkout is not proper alligned (fixed in [magento/magento2#20307](https://github.com/magento/magento2/pull/20307)) + * [#13982](https://github.com/magento/magento2/issues/13982) -- Customer Login Block sets the title for the page when rendered (fixed in [magento/magento2#20583](https://github.com/magento/magento2/pull/20583)) + * [#20773](https://github.com/magento/magento2/issues/20773) -- The autoloader throws an exception on class_exists (fixed in [magento/magento2#20950](https://github.com/magento/magento2/pull/20950)) + * [#21322](https://github.com/magento/magento2/issues/21322) -- Declarative schema: Omitting indexType throws exception (fixed in [magento/magento2#21328](https://github.com/magento/magento2/pull/21328)) + * [#15059](https://github.com/magento/magento2/issues/15059) -- Cannot reorder from the first try (fixed in [magento/magento2#21335](https://github.com/magento/magento2/pull/21335)) + * [#21359](https://github.com/magento/magento2/issues/21359) -- Search with long string display horizontal scroll in front end (fixed in [magento/magento2#21360](https://github.com/magento/magento2/pull/21360)) + * [#21365](https://github.com/magento/magento2/issues/21365) -- CSS Property name issue (fixed in [magento/magento2#21368](https://github.com/magento/magento2/pull/21368)) + * [#389](https://github.com/magento/magento2/issues/389) -- Magento_VersionsCms::widgets.css not found (fixed in [magento/graphql-ce#390](https://github.com/magento/graphql-ce/pull/390)) + * [#293](https://github.com/magento/magento2/issues/293) -- $productAttribute->getFrontend()->getSelectOptions() grabbing unnecessary options (fixed in [magento/graphql-ce#330](https://github.com/magento/graphql-ce/pull/330)) + * [#288](https://github.com/magento/magento2/issues/288) -- Add Cell Phone to Customer Address Form (fixed in [magento/graphql-ce#330](https://github.com/magento/graphql-ce/pull/330)) + * [#287](https://github.com/magento/magento2/issues/287) -- Why the status code of Web API Resource in REST always 404 (fixed in [magento/graphql-ce#330](https://github.com/magento/graphql-ce/pull/330)) + * [#286](https://github.com/magento/magento2/issues/286) -- import configurable product with file as _custom_option_type not working (fixed in [magento/graphql-ce#330](https://github.com/magento/graphql-ce/pull/330)) + * [#13937](https://github.com/magento/magento2/issues/13937) -- Sitemap filename can't exceed 32 characters (fixed in [magento/magento2#20044](https://github.com/magento/magento2/pull/20044)) + * [#20337](https://github.com/magento/magento2/issues/20337) -- Option Title breaking in two line because applying wrong css for manage width (fixed in [magento/magento2#20339](https://github.com/magento/magento2/pull/20339)) + * [#21294](https://github.com/magento/magento2/issues/21294) -- Cart can't be emptied if any product is out of stock (fixed in [magento/magento2#21295](https://github.com/magento/magento2/pull/21295)) + * [#21383](https://github.com/magento/magento2/issues/21383) -- As low as displays incorrect pricing on category page, tax appears to be added twice (fixed in [magento/magento2#21395](https://github.com/magento/magento2/pull/21395)) + * [#21398](https://github.com/magento/magento2/issues/21398) -- Doesn't show any error message when customer click on Add to cart button without selecting atleast one product from recently orderred list (fixed in [magento/magento2#21401](https://github.com/magento/magento2/pull/21401)) + * [#20310](https://github.com/magento/magento2/issues/20310) -- Cart section data has wrong product_price_value (fixed in [magento/magento2#20316](https://github.com/magento/magento2/pull/20316)) + * [#21062](https://github.com/magento/magento2/issues/21062) -- Static tests: forbid 'or' instead of '||' (fixed in [magento/magento2#21275](https://github.com/magento/magento2/pull/21275)) + * [#21154](https://github.com/magento/magento2/issues/21154) -- 2.3.0 Watermark not showing on images (fixed in [magento/magento2#21338](https://github.com/magento/magento2/pull/21338)) + * [#13338](https://github.com/magento/magento2/issues/13338) -- Products grid in admin does not display default values? (fixed in [magento/magento2#21363](https://github.com/magento/magento2/pull/21363)) + * [#21327](https://github.com/magento/magento2/issues/21327) -- Checkout Page Cancel button is not working (fixed in [magento/magento2#21356](https://github.com/magento/magento2/pull/21356)) + * [#21425](https://github.com/magento/magento2/issues/21425) -- Date design change show not correctly value in backend (fixed in [magento/magento2#21443](https://github.com/magento/magento2/pull/21443)) + * [#20078](https://github.com/magento/magento2/issues/20078) -- Magento Ui form validator message callback not supported (fixed in [magento/magento2#20079](https://github.com/magento/magento2/pull/20079)) + * [#20128](https://github.com/magento/magento2/issues/20128) -- Magento\Reports\Model\ResourceModel\Order\Collection->getDateRange() - Error in code? (fixed in [magento/magento2#20129](https://github.com/magento/magento2/pull/20129)) + * [#14857](https://github.com/magento/magento2/issues/14857) -- Sitemap generation cron job flushes entire cache (fixed in [magento/magento2#20818](https://github.com/magento/magento2/pull/20818)) + * [#21077](https://github.com/magento/magento2/issues/21077) -- Tabbing issue on product detail page (fixed in [magento/magento2#21079](https://github.com/magento/magento2/pull/21079)) + * [#21292](https://github.com/magento/magento2/issues/21292) -- Google Analytics isAnonymizedIpActive always true (fixed in [magento/magento2#21303](https://github.com/magento/magento2/pull/21303)) + * [#21454](https://github.com/magento/magento2/issues/21454) -- Infinite redirects in Magento admin (fixed in [magento/magento2#21455](https://github.com/magento/magento2/pull/21455)) + * [#283](https://github.com/magento/magento2/issues/283) -- write Product Reviews error (fixed in [magento/graphql-ce#342](https://github.com/magento/graphql-ce/pull/342)) + * [#282](https://github.com/magento/magento2/issues/282) -- Can't create configurable product (fixed in [magento/graphql-ce#342](https://github.com/magento/graphql-ce/pull/342)) + * [#281](https://github.com/magento/magento2/issues/281) -- Pre defined var __DIR__ doesn't work (fixed in [magento/graphql-ce#342](https://github.com/magento/graphql-ce/pull/342)) + * [#279](https://github.com/magento/magento2/issues/279) -- Configurable Product: Custom Options make a discount percent of Tier Price error (fixed in [magento/graphql-ce#342](https://github.com/magento/graphql-ce/pull/342)) + * [#394](https://github.com/magento/magento2/issues/394) -- update used version of phpseclib (fixed in [magento/graphql-ce#414](https://github.com/magento/graphql-ce/pull/414)) + * [#388](https://github.com/magento/magento2/issues/388) -- Why we are using umask(0) in bootstrap.php (fixed in [magento/graphql-ce#397](https://github.com/magento/graphql-ce/pull/397)) + * [#17297](https://github.com/magento/magento2/issues/17297) -- No children data for \Magento\Catalog\Model\CategoryManagement::getTree($categoryId) after first call. (fixed in [magento/magento2#18705](https://github.com/magento/magento2/pull/18705)) + * [#19632](https://github.com/magento/magento2/issues/19632) -- Backend Marketing Cart Price Rule Label Alignment Issue (fixed in [magento/magento2#19637](https://github.com/magento/magento2/pull/19637)) + * [#20187](https://github.com/magento/magento2/issues/20187) -- Downloadable product view layout (fixed in [magento/magento2#20239](https://github.com/magento/magento2/pull/20239)) + * [#19117](https://github.com/magento/magento2/issues/19117) -- High Database Load for Sales Rule Validation (fixed in [magento/magento2#20484](https://github.com/magento/magento2/pull/20484)) + * [#21278](https://github.com/magento/magento2/issues/21278) -- Sort order missing on Downloadable Product Links and Sample Columns (fixed in [magento/magento2#21279](https://github.com/magento/magento2/pull/21279)) + * [#21329](https://github.com/magento/magento2/issues/21329) -- URL Rewrites are overwritten (fixed in [magento/magento2#21462](https://github.com/magento/magento2/pull/21462)) + * [#21192](https://github.com/magento/magento2/issues/21192) -- Wrong data of Import status with Add/Update method in Advanced Prices in CSV (fixed in [magento/magento2#21476](https://github.com/magento/magento2/pull/21476)) + * [#19276](https://github.com/magento/magento2/issues/19276) -- Change different product price on selecting different product swatches on category pages (fixed in [magento/magento2#19376](https://github.com/magento/magento2/pull/19376)) + * [#20527](https://github.com/magento/magento2/issues/20527) -- [Admin] Configurable product variations table cell labels wrong position (fixed in [magento/magento2#20528](https://github.com/magento/magento2/pull/20528)) + * [#21493](https://github.com/magento/magento2/issues/21493) -- Setting default sorting (fixed in [magento/magento2#21498](https://github.com/magento/magento2/pull/21498)) + * [#21499](https://github.com/magento/magento2/issues/21499) -- Cart is emptied when enter is pressed after changing product quantity (fixed in [magento/magento2#21509](https://github.com/magento/magento2/pull/21509)) + * [#310](https://github.com/magento/magento2/issues/310) -- Problems with Controller's Router (fixed in [magento/graphql-ce#311](https://github.com/magento/graphql-ce/pull/311)) + * [#429](https://github.com/magento/magento2/issues/429) -- In dev54, the captcha of backend (Admin Login and Admin Forget Password) can't display. (fixed in [magento/graphql-ce#444](https://github.com/magento/graphql-ce/pull/444)) + * [#419](https://github.com/magento/magento2/issues/419) -- Some translation keys are not correct. (fixed in [magento/graphql-ce#451](https://github.com/magento/graphql-ce/pull/451)) + * [#424](https://github.com/magento/magento2/issues/424) -- Please combine tier pricing messages into block sentences... (fixed in [magento/graphql-ce#442](https://github.com/magento/graphql-ce/pull/442)) + * [#427](https://github.com/magento/magento2/issues/427) -- Clearing Admin notification causes "Fatal error: Maximum function nesting level of '100' reached" (fixed in [magento/graphql-ce#448](https://github.com/magento/graphql-ce/pull/448)) + * [#420](https://github.com/magento/magento2/issues/420) -- The errors happed when create new API User from backend- dev53 (fixed in [magento/graphql-ce#450](https://github.com/magento/graphql-ce/pull/450)) + * [#430](https://github.com/magento/magento2/issues/430) -- ExtJS - Update to latest version (fixed in [magento/graphql-ce#471](https://github.com/magento/graphql-ce/pull/471)) + * [#18017](https://github.com/magento/magento2/issues/18017) -- URL pre-selection of configurable product swatches with associated product images throws JavaScript error (fixed in [magento/magento2#19635](https://github.com/magento/magento2/pull/19635)) + * [#20414](https://github.com/magento/magento2/issues/20414) -- Recent orders grid not aligned from left in mobile as all content aligned from left (fixed in [magento/magento2#20429](https://github.com/magento/magento2/pull/20429)) + * [#20928](https://github.com/magento/magento2/issues/20928) -- Admin product grid Massaction design issue with sub menu (fixed in [magento/magento2#20938](https://github.com/magento/magento2/pull/20938)) + * [#20989](https://github.com/magento/magento2/issues/20989) -- Admin Customizable Options Dropdown sort_order issue (fixed in [magento/magento2#21371](https://github.com/magento/magento2/pull/21371)) + * [#21419](https://github.com/magento/magento2/issues/21419) -- Wishlist is missing review summary (fixed in [magento/magento2#21420](https://github.com/magento/magento2/pull/21420)) + * [#20809](https://github.com/magento/magento2/issues/20809) -- Advanced Search layout not proper (fixed in [magento/magento2#21611](https://github.com/magento/magento2/pull/21611)) + * [#20905](https://github.com/magento/magento2/issues/20905) -- Minicart, search & logo not vertically aligned between 640 to767 device pixel. (fixed in [magento/magento2#21638](https://github.com/magento/magento2/pull/21638)) + * [#21521](https://github.com/magento/magento2/issues/21521) -- Broken Tax Rate Search Filter - SQLSTATE[23000] (fixed in [magento/magento2#21701](https://github.com/magento/magento2/pull/21701)) + * [#13612](https://github.com/magento/magento2/issues/13612) -- 1 exception(s): Exception #0 (Exception): Warning: Illegal offset type in isset or empty in /home/jewelrynest2/public_html/magento/vendor/magento/module-eav/Model/Entity/Attribute/Source/AbstractSource.php on line 76 (fixed in [magento/magento2#20001](https://github.com/magento/magento2/pull/20001)) + * [#18761](https://github.com/magento/magento2/issues/18761) -- Bug with REPLACE method in Advanced Prices in CSV Import (fixed in [magento/magento2#21189](https://github.com/magento/magento2/pull/21189)) + * [#21384](https://github.com/magento/magento2/issues/21384) -- JS minify field is not disabled in developer configuration (fixed in [magento/magento2#21444](https://github.com/magento/magento2/pull/21444)) + * [#21541](https://github.com/magento/magento2/issues/21541) -- Whitespace issues for related, cross and upsell grids (fixed in [magento/magento2#21582](https://github.com/magento/magento2/pull/21582)) + * [#167](https://github.com/magento/magento2/issues/167) -- Fatal error: Class 'Mage' not found (fixed in [magento/magento2#21731](https://github.com/magento/magento2/pull/21731)) + * [#20511](https://github.com/magento/magento2/issues/20511) -- Sorting by 'Websites' not working in product grid in backoffice (fixed in [magento/magento2#20512](https://github.com/magento/magento2/pull/20512)) + * [#19360](https://github.com/magento/magento2/issues/19360) -- Missed form validation in Admin Order Address Edit route sales/order/address (fixed in [magento/magento2#20840](https://github.com/magento/magento2/pull/20840)) + * [#17295](https://github.com/magento/magento2/issues/17295) -- Search REST API returns wrong total_count (fixed in [magento/magento2#21713](https://github.com/magento/magento2/pull/21713)) + * [#18630](https://github.com/magento/magento2/issues/18630) -- Postcode / Zipcode in checkout form already validated on page load (fixed in [magento/magento2#18633](https://github.com/magento/magento2/pull/18633)) + * [#21648](https://github.com/magento/magento2/issues/21648) -- Checkout Agreements checkbox missing asterisk (fixed in [magento/magento2#21649](https://github.com/magento/magento2/pull/21649)) + * [#12396](https://github.com/magento/magento2/issues/12396) -- "Total Amount" cart rule without tax (fixed in [magento/magento2#21288](https://github.com/magento/magento2/pull/21288)) + * [#21467](https://github.com/magento/magento2/issues/21467) -- Tier price of simple item not working in Bundle product (fixed in [magento/magento2#21469](https://github.com/magento/magento2/pull/21469)) + * [#21510](https://github.com/magento/magento2/issues/21510) -- Can't access backend indexers page after creating a custom index (fixed in [magento/magento2#21575](https://github.com/magento/magento2/pull/21575)) + * [#21750](https://github.com/magento/magento2/issues/21750) -- Product attribute labels are translated (fixed in [magento/magento2#21751](https://github.com/magento/magento2/pull/21751)) + * [#19835](https://github.com/magento/magento2/issues/19835) -- Admin grid button flicker issue after page load due to re-ordering (fixed in [magento/magento2#21791](https://github.com/magento/magento2/pull/21791)) + * [#21374](https://github.com/magento/magento2/issues/21374) -- Dot is not allowed when editing CMS block in-line (fixed in [magento/magento2#21376](https://github.com/magento/magento2/pull/21376)) + * [#21396](https://github.com/magento/magento2/issues/21396) -- [Frontend] Additional addresses DataTable Pagination count displaying wrong. (fixed in [magento/magento2#21399](https://github.com/magento/magento2/pull/21399)) + * [#21692](https://github.com/magento/magento2/issues/21692) -- Incorrect constructor of Magento\Sales\Model\Order\Address\Validator (fixed in [magento/magento2#21693](https://github.com/magento/magento2/pull/21693)) + * [#21752](https://github.com/magento/magento2/issues/21752) -- Error while installing Magento from scratch if Locale Resolver is injected in cli command (fixed in [magento/magento2#21693](https://github.com/magento/magento2/pull/21693)) + * [#20825](https://github.com/magento/magento2/issues/20825) -- Missing required argument $productAvailabilityChecks of Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityChecker. (fixed in [magento/magento2#21820](https://github.com/magento/magento2/pull/21820)) + * [#20859](https://github.com/magento/magento2/issues/20859) -- Luma theme - Input Box and Radio Button shadow is not proper (fixed in [magento/magento2#21851](https://github.com/magento/magento2/pull/21851)) + * [#482](https://github.com/magento/magento2/issues/482) -- Cms pages meta title (fixed in [magento/graphql-ce#492](https://github.com/magento/graphql-ce/pull/492)) + * [#20209](https://github.com/magento/magento2/issues/20209) -- errors/local.xml and error page templates are publicly accessible (fixed in [magento/magento2#20212](https://github.com/magento/magento2/pull/20212)) + * [#20434](https://github.com/magento/magento2/issues/20434) -- Product URL duplicate when changing visibility via mass action (fixed in [magento/magento2#20774](https://github.com/magento/magento2/pull/20774)) + * [#18754](https://github.com/magento/magento2/issues/18754) -- Negative order amount in dashboard latest order when order is cancelled where coupon has been used (fixed in [magento/magento2#21283](https://github.com/magento/magento2/pull/21283)) + * [#21281](https://github.com/magento/magento2/issues/21281) -- Wrong order amount on dashboard on Last orders listing when order has discount and it is partially refunded (fixed in [magento/magento2#21283](https://github.com/magento/magento2/pull/21283)) + * [#21620](https://github.com/magento/magento2/issues/21620) -- Update title of Review content (fixed in [magento/magento2#21621](https://github.com/magento/magento2/pull/21621)) + * [#21001](https://github.com/magento/magento2/issues/21001) -- Unit Tests failed (fixed in [magento/magento2#21880](https://github.com/magento/magento2/pull/21880)) + * [#432](https://github.com/magento/magento2/issues/432) -- [Feature request] Make reindex for specific store view (fixed in [magento/graphql-ce#449](https://github.com/magento/graphql-ce/pull/449)) + * [#14926](https://github.com/magento/magento2/issues/14926) -- "Rolled back transaction has not been completed correctly" on Magento 2.2.3 (fixed in [magento/magento2#21697](https://github.com/magento/magento2/pull/21697)) + * [#18752](https://github.com/magento/magento2/issues/18752) -- Rolled back transaction has not been completed correctly" on Magento 2.1.15 (fixed in [magento/magento2#21697](https://github.com/magento/magento2/pull/21697)) + * [#21734](https://github.com/magento/magento2/issues/21734) -- Error in JS validation rule (fixed in [magento/magento2#21776](https://github.com/magento/magento2/pull/21776)) + * [#422](https://github.com/magento/magento2/issues/422) -- Cannot run a new module installation script after Magento 2 installation (fixed in [magento/graphql-ce#467](https://github.com/magento/graphql-ce/pull/467)) + * [#478](https://github.com/magento/magento2/issues/478) -- wishlist cannot get product item caused the fatal error (fixed in [magento/graphql-ce#491](https://github.com/magento/graphql-ce/pull/491)) + * [#485](https://github.com/magento/magento2/issues/485) -- Problem with configuration of SetupFactory in di.xml (fixed in [magento/graphql-ce#496](https://github.com/magento/graphql-ce/pull/496)) + * [#15972](https://github.com/magento/magento2/issues/15972) -- Since Magento 2.2.1, certain variables in the configuration get resolved to their actual value (fixed in [magento/magento2#18067](https://github.com/magento/magento2/pull/18067)) + * [#17658](https://github.com/magento/magento2/issues/17658) -- validate function in vatvalidation calls checkVatNumber a lot (fixed in [magento/magento2#19265](https://github.com/magento/magento2/pull/19265)) + * [#20766](https://github.com/magento/magento2/issues/20766) -- AttributeCode column name length validation throws wrong error message (fixed in [magento/magento2#20526](https://github.com/magento/magento2/pull/20526)) + * [#20943](https://github.com/magento/magento2/issues/20943) -- No complete validation while creation of attributes. (fixed in [magento/magento2#20526](https://github.com/magento/magento2/pull/20526)) + * [#13319](https://github.com/magento/magento2/issues/13319) -- Incorrect method return value in \Magento\Shipping\Model\Carrier\AbstractCarrier::getTotalNumOfBoxes() (fixed in [magento/magento2#20898](https://github.com/magento/magento2/pull/20898)) + * [#21134](https://github.com/magento/magento2/issues/21134) -- Invalid argument supplied for foreach thrown in EAV code (fixed in [magento/magento2#21135](https://github.com/magento/magento2/pull/21135)) + * [#10893](https://github.com/magento/magento2/issues/10893) -- Street fields in checkout don't have a label that's readable by a screenreader (fixed in [magento/magento2#21484](https://github.com/magento/magento2/pull/21484)) + * [#21805](https://github.com/magento/magento2/issues/21805) -- Filter in url rewrites table in backend isn't being remembered (fixed in [magento/magento2#21834](https://github.com/magento/magento2/pull/21834)) + * [#423](https://github.com/magento/magento2/issues/423) -- Can't login backend after running some time - dev53 (fixed in [magento/graphql-ce#460](https://github.com/magento/graphql-ce/pull/460)) + * [#13951](https://github.com/magento/magento2/issues/13951) -- Exception on customer edit under restricted admin access (fixed in [magento/magento2#18386](https://github.com/magento/magento2/pull/18386)) + * [#19761](https://github.com/magento/magento2/issues/19761) -- Custom import adapter data validation issue (fixed in [magento/magento2#19765](https://github.com/magento/magento2/pull/19765)) + * [#21755](https://github.com/magento/magento2/issues/21755) -- Magento should create a log entry if an observer does not implement ObserverInterface (fixed in [magento/magento2#21767](https://github.com/magento/magento2/pull/21767)) + * [#295](https://github.com/magento/magento2/issues/295) -- [Backend] System Configuration UI issues (fixed in [magento/graphql-ce#404](https://github.com/magento/graphql-ce/pull/404)) + * [#19909](https://github.com/magento/magento2/issues/19909) -- Not possible to use multidimensional arrays in widget parameters (fixed in [magento/magento2#21008](https://github.com/magento/magento2/pull/21008)) + * [#21926](https://github.com/magento/magento2/issues/21926) -- Exception on reorder from admin (fixed in [magento/magento2#21928](https://github.com/magento/magento2/pull/21928)) + * [#20140](https://github.com/magento/magento2/issues/20140) -- Product per row not proper on listing page (fixed in [magento/magento2#21948](https://github.com/magento/magento2/pull/21948)) + * [#21244](https://github.com/magento/magento2/issues/21244) -- Luma theme huge whitespace on category grid (fixed in [magento/magento2#21948](https://github.com/magento/magento2/pull/21948)) + * [#512](https://github.com/magento/magento2/issues/512) -- Theme Thumbnails not showing (fixed in [magento/graphql-ce#562](https://github.com/magento/graphql-ce/pull/562)) + * [#479](https://github.com/magento/magento2/issues/479) -- Different locale Settings don't work (fixed in [magento/graphql-ce#558](https://github.com/magento/graphql-ce/pull/558)) + * [#21789](https://github.com/magento/magento2/issues/21789) -- [BUG] Product gallery opening by mistake (fixed in [magento/magento2#21790](https://github.com/magento/magento2/pull/21790)) + * [#21998](https://github.com/magento/magento2/issues/21998) -- Magento/ImportExport/Model/Import has _coreConfig declared dynamically (fixed in [magento/magento2#21999](https://github.com/magento/magento2/pull/21999)) + * [#21993](https://github.com/magento/magento2/issues/21993) -- config:set not storing scoped values (fixed in [magento/magento2#22012](https://github.com/magento/magento2/pull/22012)) + * [#20186](https://github.com/magento/magento2/issues/20186) -- phpcs error on rule classes - must be of the type integer (fixed in [magento/magento2#22081](https://github.com/magento/magento2/pull/22081)) + * [#20366](https://github.com/magento/magento2/issues/20366) -- The parent product doesn't have configurable product options. (fixed in [magento/magento2#21083](https://github.com/magento/magento2/pull/21083)) + * [#22001](https://github.com/magento/magento2/issues/22001) -- Magento backend dashboard: Most viewed products tabs gives 404 error in console. (fixed in [magento/magento2#22002](https://github.com/magento/magento2/pull/22002)) + * [#581](https://github.com/magento/magento2/issues/581) -- About ByPercent.php under different currencies (fixed in [magento/graphql-ce#586](https://github.com/magento/graphql-ce/pull/586)) + * [#21916](https://github.com/magento/magento2/issues/21916) -- Elasticsearch6 generation does not exist (fixed in [magento/magento2#22046](https://github.com/magento/magento2/pull/22046)) + * [#21976](https://github.com/magento/magento2/issues/21976) -- Magento doesn't work after upgrade from 2.3.0 to 2.3.1 (fixed in [magento/magento2#22046](https://github.com/magento/magento2/pull/22046)) + * [#21715](https://github.com/magento/magento2/issues/21715) -- Previous scrolling to invalid form element is not being canceled on hitting submit multiple times (fixed in [magento/magento2#22117](https://github.com/magento/magento2/pull/22117)) + * [#21824](https://github.com/magento/magento2/issues/21824) -- Filter in admin users grid in backend isn't being remembered (fixed in [magento/magento2#22128](https://github.com/magento/magento2/pull/22128)) + * [#21973](https://github.com/magento/magento2/issues/21973) -- Why phar stream is being unregistered? (fixed in [magento/magento2#22171](https://github.com/magento/magento2/pull/22171)) + * [#22166](https://github.com/magento/magento2/issues/22166) -- Information and link in README.md file related to Security issue reporting should be updated (fixed in [magento/magento2#22195](https://github.com/magento/magento2/pull/22195)) + * [#7623](https://github.com/magento/magento2/issues/7623) -- Web Setup Wizard not visible in backend (V.2.1.2) ONGOING (fixed in [magento/magento2#20182](https://github.com/magento/magento2/pull/20182)) + * [#11892](https://github.com/magento/magento2/issues/11892) -- Web Setup Wizard not visible in backend magento 2.1.9 (fixed in [magento/magento2#20182](https://github.com/magento/magento2/pull/20182)) + * [#20830](https://github.com/magento/magento2/issues/20830) -- On Header customer name appearing twice after login (fixed in [magento/magento2#20832](https://github.com/magento/magento2/pull/20832)) + * [#21375](https://github.com/magento/magento2/issues/21375) -- Same product quantity not increment when added with guest user. (fixed in [magento/magento2#21501](https://github.com/magento/magento2/pull/21501)) + * [#21786](https://github.com/magento/magento2/issues/21786) -- Asynchronous email sending for the sales entities which were created with disabled email sending (fixed in [magento/magento2#21788](https://github.com/magento/magento2/pull/21788)) + * [#21753](https://github.com/magento/magento2/issues/21753) -- Order Item Status to Enable Downloads is set to "Pending," but no download links are presented in "My Downloads" when logged in (fix provided) (fixed in [magento/magento2#22073](https://github.com/magento/magento2/pull/22073)) + * [#426](https://github.com/magento/magento2/issues/426) -- The 'register' should be 'Register' in default.xml of pluse theme (fixed in [magento/graphql-ce#441](https://github.com/magento/graphql-ce/pull/441)) + * [#425](https://github.com/magento/magento2/issues/425) -- Installation of dev53 fails (fixed in [magento/graphql-ce#441](https://github.com/magento/graphql-ce/pull/441)) + * [#564](https://github.com/magento/magento2/issues/564) -- Catalog product images - Do not removing from file system (fixed in [magento/graphql-ce#571](https://github.com/magento/graphql-ce/pull/571) and [magento/graphql-ce#614](https://github.com/magento/graphql-ce/pull/614)) + * [#12386](https://github.com/magento/magento2/issues/12386) -- Order Status resets to default Status after Partial Refund (fixed in [magento/magento2#20378](https://github.com/magento/magento2/pull/20378)) + * [#16513](https://github.com/magento/magento2/issues/16513) -- Can not save an inactive Admin User that has no access tokens generated (fixed in [magento/magento2#20772](https://github.com/magento/magento2/pull/20772)) + * [#21868](https://github.com/magento/magento2/issues/21868) -- Method importFromArray from \Magento\Eav\Model\Entity\Collection\AbstractCollection doesn't return a working collection (fixed in [magento/magento2#21869](https://github.com/magento/magento2/pull/21869)) + * [#22030](https://github.com/magento/magento2/issues/22030) -- Typo issue: Magento admin sales order shipment header typo issue (fixed in [magento/magento2#22031](https://github.com/magento/magento2/pull/22031)) + * [#22090](https://github.com/magento/magento2/issues/22090) -- MsrpPriceCalculator exception after upgrade to 2.3.1 (fixed in [magento/magento2#22197](https://github.com/magento/magento2/pull/22197)) + * [#22190](https://github.com/magento/magento2/issues/22190) -- Exception (BadMethodCallException): Missing required argument $msrpPriceCalculators of Magento\Msrp\Pricing\MsrpPriceCalculator. (fixed in [magento/magento2#22197](https://github.com/magento/magento2/pull/22197)) + * [#18557](https://github.com/magento/magento2/issues/18557) -- Value of created_at and updated_at columns not updating in ui_bookmark table (fixed in [magento/magento2#22340](https://github.com/magento/magento2/pull/22340)) + * [#21299](https://github.com/magento/magento2/issues/21299) -- HEAD request returns 404 (fixed in [magento/magento2#21378](https://github.com/magento/magento2/pull/21378)) + * [#21907](https://github.com/magento/magento2/issues/21907) -- Place order button disabled after failed email address validation check with braintree credit card (fixed in [magento/magento2#21936](https://github.com/magento/magento2/pull/21936)) + * [#6715](https://github.com/magento/magento2/issues/6715) -- Few weaknesses in the code (fixed in [magento/magento2#21968](https://github.com/magento/magento2/pull/21968)) + * [#21960](https://github.com/magento/magento2/issues/21960) -- Layered Navigation: “Equalize product count” not working as expected (fixed in [magento/magento2#21968](https://github.com/magento/magento2/pull/21968)) + * [#22152](https://github.com/magento/magento2/issues/22152) -- Click on search icon it does not working (fixed in [magento/magento2#22154](https://github.com/magento/magento2/pull/22154)) + * [#22199](https://github.com/magento/magento2/issues/22199) -- A bug with health_check.php (fixed in [magento/magento2#22200](https://github.com/magento/magento2/pull/22200)) + * [#15090](https://github.com/magento/magento2/issues/15090) -- app:config:import fails with "Please specify the admin custom URL." (fixed in [magento/magento2#22281](https://github.com/magento/magento2/pull/22281)) + * [#20917](https://github.com/magento/magento2/issues/20917) -- Alignment Issue While Editing Order Data containing Downlodable Products in Admin Section (fixed in [magento/magento2#22298](https://github.com/magento/magento2/pull/22298)) + * [#21747](https://github.com/magento/magento2/issues/21747) -- catalog_product_flat_data for store view populated with default view data when it should be store view data (fixed in [magento/magento2#22318](https://github.com/magento/magento2/pull/22318)) + * [#22317](https://github.com/magento/magento2/issues/22317) -- CodeSniffer should not mark correctly aligned DocBlock elements as code style violation. (fixed in [magento/magento2#22321](https://github.com/magento/magento2/pull/22321)) + * [#22330](https://github.com/magento/magento2/issues/22330) -- Error "extend ' .no-link a' has no matches" when compiling email-inline css using an alternative less compiler (fixed in [magento/magento2#22332](https://github.com/magento/magento2/pull/22332)) + * [#22309](https://github.com/magento/magento2/issues/22309) -- Category Update without "name" cannot be saved in scope "all" with REST API (fixed in [magento/magento2#22362](https://github.com/magento/magento2/pull/22362)) + * [#607](https://github.com/magento/magento2/issues/607) -- sitemap.xml filename is not variable (fixed in [magento/graphql-ce#610](https://github.com/magento/graphql-ce/pull/610)) + * [#605](https://github.com/magento/magento2/issues/605) -- tinyMceWysiwyg is not working in admin form edit (fixed in [magento/graphql-ce#616](https://github.com/magento/graphql-ce/pull/616)) + * [#604](https://github.com/magento/magento2/issues/604) -- 'Continue' button is disabled even though 'I've read OSL licence' is checked (fixed in [magento/graphql-ce#614](https://github.com/magento/graphql-ce/pull/614)) + * [#19825](https://github.com/magento/magento2/issues/19825) -- Magento 2.3.0: Backup tool not correctly detecting .maintenance.flag (fixed in [magento/magento2#19993](https://github.com/magento/magento2/pull/19993)) + * [#13409](https://github.com/magento/magento2/issues/13409) -- custom widget with wysiwyg problem on insert widget via pages or blocks (fixed in [magento/magento2#20174](https://github.com/magento/magento2/pull/20174)) + * [#19742](https://github.com/magento/magento2/issues/19742) -- Widgets with a WYSIWYG parameter fail when inserting them into a WYSIWYG in a form. (fixed in [magento/magento2#20174](https://github.com/magento/magento2/pull/20174)) + * [#21654](https://github.com/magento/magento2/issues/21654) -- \Magento\Framework\Data\Collection::clear does not clear the result for \Magento\Framework\Data\Collection::getSize (fixed in [magento/magento2#21670](https://github.com/magento/magento2/pull/21670)) + * [#21702](https://github.com/magento/magento2/issues/21702) -- Purchasing a downloadable product as guest then creating an account on the onepagesuccess step doesn't link product with account (fixed in [magento/magento2#21711](https://github.com/magento/magento2/pull/21711)) + * [#21779](https://github.com/magento/magento2/issues/21779) -- Adminhtml textarea field doesn't accept maxlength (fixed in [magento/magento2#21816](https://github.com/magento/magento2/pull/21816)) + * [#22246](https://github.com/magento/magento2/issues/22246) -- Programatically created invoices miss items when both simple products and bundled products are mixed in an order (fixed in [magento/magento2#22263](https://github.com/magento/magento2/pull/22263)) + * [#22355](https://github.com/magento/magento2/issues/22355) -- Import product quantity is empty after import (fixed in [magento/magento2#22382](https://github.com/magento/magento2/pull/22382)) + * [#6272](https://github.com/magento/magento2/issues/6272) -- Changing sample for downloadable product failure (fixed in [magento/magento2#19806](https://github.com/magento/magento2/pull/19806)) + * [#3283](https://github.com/magento/magento2/issues/3283) -- «Yes/No» attributes should be allowed in the Layered Navigation (fixed in [magento/magento2#21772](https://github.com/magento/magento2/pull/21772)) + * [#21771](https://github.com/magento/magento2/issues/21771) -- Performance degradation in Layered navigation using Yes/No attribute (fixed in [magento/magento2#21772](https://github.com/magento/magento2/pull/21772)) + * [#8035](https://github.com/magento/magento2/issues/8035) -- Join extension attributes are not added to Order results (REST api) (fixed in [magento/magento2#21797](https://github.com/magento/magento2/pull/21797)) + * [#22223](https://github.com/magento/magento2/issues/22223) -- Missing/Wrong data display on downloadable report table reports>downloads in BO (fixed in [magento/magento2#22291](https://github.com/magento/magento2/pull/22291)) + * [#7227](https://github.com/magento/magento2/issues/7227) -- "x_forwarded_for" value is always empty in Order object. (fixed in [magento/magento2#21787](https://github.com/magento/magento2/pull/21787)) + * [#22047](https://github.com/magento/magento2/issues/22047) -- Magento CRON Job Names are missing in NewRelic: "Transaction Names" (fixed in [magento/magento2#22059](https://github.com/magento/magento2/pull/22059)) + * [#21737](https://github.com/magento/magento2/issues/21737) -- Duplicating product with translated url keys over multiple storeviews causes non-unique url keys to be generated (fixed in [magento/magento2#22178](https://github.com/magento/magento2/pull/22178)) + * [#22474](https://github.com/magento/magento2/issues/22474) -- Incomplete Dependency on Backup Settings Configuration (fixed in [magento/magento2#22475](https://github.com/magento/magento2/pull/22475)) + * [#22402](https://github.com/magento/magento2/issues/22402) -- PUT /V1/products/:sku/media/:entryId does not change the file (fixed in [magento/magento2#22424](https://github.com/magento/magento2/pull/22424)) + * [#22124](https://github.com/magento/magento2/issues/22124) -- Magento 2.3.1: Catalog setup fails with error "Magento\Catalog\Setup\Media does not exist" (fixed in [magento/magento2#22446](https://github.com/magento/magento2/pull/22446)) + * [#22434](https://github.com/magento/magento2/issues/22434) -- While add cart price rule from admin click on Condition drop-down arrow direction not change. (fixed in [magento/magento2#22456](https://github.com/magento/magento2/pull/22456)) + * [#20111](https://github.com/magento/magento2/issues/20111) -- Email Template Information Insert Variable popup blank (fixed in [magento/magento2#22469](https://github.com/magento/magento2/pull/22469)) + * [#21147](https://github.com/magento/magento2/issues/21147) -- Can't scroll in modal-popup on iOS (fixed in [magento/magento2#21150](https://github.com/magento/magento2/pull/21150)) + * [#21962](https://github.com/magento/magento2/issues/21962) -- Magento Sales Order: Design Align issue (fixed in [magento/magento2#21963](https://github.com/magento/magento2/pull/21963)) + * [#19544](https://github.com/magento/magento2/issues/19544) -- Grunt watch triggers entire page reload (fixed in [magento/magento2#22276](https://github.com/magento/magento2/pull/22276)) + * [#22299](https://github.com/magento/magento2/issues/22299) -- Cms block cache key does not contain the store id (fixed in [magento/magento2#22302](https://github.com/magento/magento2/pull/22302)) + * [#22270](https://github.com/magento/magento2/issues/22270) -- 2.2.8 Configurable product option dropdown - price difference incorrect when catalog prices are entered excluding tax (fixed in [magento/magento2#22466](https://github.com/magento/magento2/pull/22466)) + * [#9155](https://github.com/magento/magento2/issues/9155) -- Adding product from wishlist not adding to cart showing warning message. (fixed in [magento/magento2#19653](https://github.com/magento/magento2/pull/19653)) + * [#20481](https://github.com/magento/magento2/issues/20481) -- REST products update category_ids cannot be removed (fixed in [magento/magento2#20842](https://github.com/magento/magento2/pull/20842)) + * [#21477](https://github.com/magento/magento2/issues/21477) -- Magento 2.3 quote_item table has incorrect default value in declarative schema (fixed in [magento/magento2#21486](https://github.com/magento/magento2/pull/21486)) + * [#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#21633](https://github.com/magento/magento2/pull/21633)) + * [#19908](https://github.com/magento/magento2/issues/19908) -- REST-API locale is always default scope (fixed in [magento/magento2#19913](https://github.com/magento/magento2/pull/19913)) + * [#21842](https://github.com/magento/magento2/issues/21842) -- Checkout error for registered customer with cache_id_prefix on multi server setup (fixed in [magento/magento2#21856](https://github.com/magento/magento2/pull/21856)) + * [#21032](https://github.com/magento/magento2/issues/21032) -- Error on design configuration save with imageUploader form element populated from gallery (fixed in [magento/magento2#22132](https://github.com/magento/magento2/pull/22132)) + * [#22052](https://github.com/magento/magento2/issues/22052) -- Customer account confirmation is overwritten by backend customer save (fixed in [magento/magento2#22147](https://github.com/magento/magento2/pull/22147)) + * [#12802](https://github.com/magento/magento2/issues/12802) -- QuoteRepository get methods won't return CartInterface but Quote model (fixed in [magento/magento2#22149](https://github.com/magento/magento2/pull/22149)) + * [#21596](https://github.com/magento/magento2/issues/21596) -- Checkout: it is possible to leave blank Shipping Details section and get to Payment Details section by URL (fixed in [magento/magento2#22405](https://github.com/magento/magento2/pull/22405)) +* GitHub pull requests: + * [magento/magento2#18706](https://github.com/magento/magento2/pull/18706) -- icon text showing feature (by @Karlasa) + * [magento/magento2#19546](https://github.com/magento/magento2/pull/19546) -- Fix typo in SQL join when joining custom option prices for price indexer (by @udovicic) + * [magento/magento2#19598](https://github.com/magento/magento2/pull/19598) -- Images in XML sitemap are always linked to base store in multistore on Schedule (by @Nazar65) + * [magento/magento2#20011](https://github.com/magento/magento2/pull/20011) -- Issue fix #20010 Wrong price amount in opengraph (by @milindsingh) + * [magento/magento2#20092](https://github.com/magento/magento2/pull/20092) -- Fix error on logo upload for Transactional Emails (#20091) (by @chaplynsky) + * [magento/magento2#20173](https://github.com/magento/magento2/pull/20173) -- [Forwardport] 'customer-login-page-input-field-are-short-width-on-tablet-view' :: a… (by @nainesh2jcommerce) + * [magento/magento2#20381](https://github.com/magento/magento2/pull/20381) -- Order shipping method bug (by @maheshWebkul721) + * [magento/magento2#20461](https://github.com/magento/magento2/pull/20461) -- #20376 Fix issue with file uploading if an upload field is disabled (by @serhiyzhovnir) + * [magento/magento2#20556](https://github.com/magento/magento2/pull/20556) -- Fixed issue #20555 Meta Keywords/Meta Description are input field in product form while they are defined as textarea (by @amitcedcoss) + * [magento/magento2#20992](https://github.com/magento/magento2/pull/20992) -- focus-not-proper-on-configurable-product-swatches:: focus not proper … (by @nainesh2jcommerce) + * [magento/magento2#21055](https://github.com/magento/magento2/pull/21055) -- added min=0 to qty field product detail page (by @awviraj) + * [magento/magento2#21120](https://github.com/magento/magento2/pull/21120) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#19631](https://github.com/magento/magento2/pull/19631) -- Backend: Fixed checkbox alignment (by @suryakant-krish) + * [magento/magento2#20012](https://github.com/magento/magento2/pull/20012) -- typos corrected (by @mjsachan-cedcoss) + * [magento/magento2#20027](https://github.com/magento/magento2/pull/20027) -- hardcoded table name (by @melaxon) + * [magento/magento2#20115](https://github.com/magento/magento2/pull/20115) -- Fixed Issue #19891 ,Added checks of type_id (by @GovindaSharma) + * [magento/magento2#20499](https://github.com/magento/magento2/pull/20499) -- Fix typo in _resets.less (by @sjaakvdbrom) + * [magento/magento2#20887](https://github.com/magento/magento2/pull/20887) -- Removed github oauth token in sample file. The token is a personal to… (by @hostep) + * [magento/magento2#21007](https://github.com/magento/magento2/pull/21007) -- disable add to cart until page load (by @sunilit42) + * [magento/magento2#21022](https://github.com/magento/magento2/pull/21022) -- products-in-category-checkbox-not-align-properly (by @priti2jcommerce) + * [magento/magento2#21090](https://github.com/magento/magento2/pull/21090) -- Add alt text to saved payment method for accessibility (by @pmclain) + * [magento/magento2#21097](https://github.com/magento/magento2/pull/21097) -- Apply PHP-CS-Fixer "braces" fixes on `if` and `foreach` statements (by @yogeshsuhagiya) + * [magento/magento2#21096](https://github.com/magento/magento2/pull/21096) -- Updated sprintf usage; Simplified isset usage (by @df2k2) + * [magento/magento2#21100](https://github.com/magento/magento2/pull/21100) -- [Sales] Improves the UX by scrolling down the customer to the Recent Orders (by @eduard13) + * [magento/magento2#21129](https://github.com/magento/magento2/pull/21129) -- Remove unused reference on wishlist ConvertSerializedData controller (by @sasangagamlath) + * [magento/magento2#20165](https://github.com/magento/magento2/pull/20165) -- issue fixed #20163 On iPhone5 device newsletter subscription input bo... (by @cedarvinda) + * [magento/magento2#20466](https://github.com/magento/magento2/pull/20466) -- view-order-price-subtotal-alignment-not-proper-mobile (by @priti2jcommerce) + * [magento/magento2#20682](https://github.com/magento/magento2/pull/20682) -- Full Tax Summary display wrong numbers (by @niravkrish) + * [magento/magento2#20847](https://github.com/magento/magento2/pull/20847) -- Fixed validation strategy label in import form (by @elevinskii) + * [magento/magento2#20881](https://github.com/magento/magento2/pull/20881) -- Add the ability to disable/remove an action from Mass(Tree)Action (by @Beagon) + * [magento/magento2#20896](https://github.com/magento/magento2/pull/20896) -- Fixed #17861 Customer Name Prefix shows white space when extra separator is addes (by @shikhamis11) + * [magento/magento2#20895](https://github.com/magento/magento2/pull/20895) -- Entered data missing when entering the wrong date for from, to in cart rule (by @realadityayadav) + * [magento/magento2#20902](https://github.com/magento/magento2/pull/20902) -- Fixed #17564 Magento 2 inline edit date issues in admin grid with Ui Component (by @satyaprakashpatel) + * [magento/magento2#20953](https://github.com/magento/magento2/pull/20953) -- #18698 Fixed order email sending via order async email sending when order was created with disabled email sending (by @serhiyzhovnir) + * [magento/magento2#20963](https://github.com/magento/magento2/pull/20963) -- bundle-product-table-data-grouped-alignment :: Bundle product table d… (by @parag2jcommerce) + * [magento/magento2#21009](https://github.com/magento/magento2/pull/21009) -- issue fixed #20919 Email label and email field not aligned from left ... (by @cedarvinda) + * [magento/magento2#21038](https://github.com/magento/magento2/pull/21038) -- quantity-not-center-align-on-review-order (by @nainesh2jcommerce) + * [magento/magento2#21071](https://github.com/magento/magento2/pull/21071) -- Fixed Luma theme my account Order status tabs 21070 (by @abrarpathan19) + * [magento/magento2#21102](https://github.com/magento/magento2/pull/21102) -- [Catalog] Fixing compare block product removing action from sidebar (by @eduard13) + * [magento/magento2#21145](https://github.com/magento/magento2/pull/21145) -- Fixed #21144 Can't change customer group when placing an admin order (by @gauravagarwal1001) + * [magento/magento2#21165](https://github.com/magento/magento2/pull/21165) -- Fix tests breaking when upgrading from 2.2 to 2.3 (by @navarr) + * [magento/magento2#18852](https://github.com/magento/magento2/pull/18852) -- Changes cache hosts warning / critical levels and continue on multiple hosts (by @wiardvanrij) + * [magento/magento2#19608](https://github.com/magento/magento2/pull/19608) -- Fixed Custom option price calculation is wrong with multi currency when option price type is percentage (by @emiprotech) + * [magento/magento2#19996](https://github.com/magento/magento2/pull/19996) -- Fixed issue Unable to open URL for downloadable product (by @shikhamis11) + * [magento/magento2#20495](https://github.com/magento/magento2/pull/20495) -- #18347 - Element 'css', attribute 'as': The attribute 'as' is not allowed. (CSS preloading) (by @vasilii-b) + * [magento/magento2#20923](https://github.com/magento/magento2/pull/20923) -- Fixed issue if there are multiple skus in catalog rule condition combination (by @suneet64) + * [magento/magento2#21069](https://github.com/magento/magento2/pull/21069) -- Error icon issue resolved (by @speedy008) + * [magento/magento2#21093](https://github.com/magento/magento2/pull/21093) -- Removed useless sprintf and removed code no longer needed (by @df2k2) + * [magento/magento2#21095](https://github.com/magento/magento2/pull/21095) -- Fixing returning types (by @eduard13) + * [magento/magento2#21098](https://github.com/magento/magento2/pull/21098) -- Updated Deprecated functions call (by @ankitsrivastavacedcoss) + * [magento/magento2#19359](https://github.com/magento/magento2/pull/19359) -- Removed direct use of SessionManager class, used SessionManagerInterface instead (by @jaimin-ktpl) + * [magento/magento2#21260](https://github.com/magento/magento2/pull/21260) -- Code clean for page doc comment on select.test.js (by @lpj822) + * [magento/magento2#19191](https://github.com/magento/magento2/pull/19191) -- Customer related values are NULL for guests converted to customers after checkout. #19166 (by @Nazar65) + * [magento/magento2#19487](https://github.com/magento/magento2/pull/19487) -- Fix DHL Quotes for Domestic Shipments when Content Type is set to Non-Document (by @gwharton) + * [magento/magento2#19566](https://github.com/magento/magento2/pull/19566) -- Minimum Qty Allowed in Shopping Cart not working on related product (by @mageprince) + * [magento/magento2#19679](https://github.com/magento/magento2/pull/19679) -- #19575 magentoDataFixture should allow to use Module Prefix - Integrations Test (by @larsroettig) + * [magento/magento2#20237](https://github.com/magento/magento2/pull/20237) -- Backend: User Role Checkbox alignement. (by @suryakant-krish) + * [magento/magento2#20839](https://github.com/magento/magento2/pull/20839) -- Checkout shipping tooltip 20838 (by @abrarpathan19) + * [magento/magento2#21197](https://github.com/magento/magento2/pull/21197) -- [Ui] Fixing the changing state of dropdown's icon (by @eduard13) + * [magento/magento2#21227](https://github.com/magento/magento2/pull/21227) -- remove-duplicated-media (by @priti2jcommerce) + * [magento/magento2#19505](https://github.com/magento/magento2/pull/19505) -- ISSUE-5021 fixed guest checkout with custom shipping carrier with unde... (by @vovsky) + * [magento/magento2#21046](https://github.com/magento/magento2/pull/21046) -- Remove unwanted condition check (by @dominicfernando) + * [magento/magento2#21121](https://github.com/magento/magento2/pull/21121) -- Applied PHP-CS-Fixer: concat_space, no_multiline_whitespace_around_double_arrow, ordered_imports (by @yogeshsuhagiya) + * [magento/magento2#21178](https://github.com/magento/magento2/pull/21178) -- Fix issue 21177 - Cart page cross-sell product add-to-cart button issue resolved (by @speedy008) + * [magento/magento2#21210](https://github.com/magento/magento2/pull/21210) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#20971](https://github.com/magento/magento2/pull/20971) -- Cast attribute ID to integer - Fixes #20969 (by @k4emic) + * [magento/magento2#21175](https://github.com/magento/magento2/pull/21175) -- Added translation for comment tag (by @yogeshsuhagiya) + * [magento/magento2#21265](https://github.com/magento/magento2/pull/21265) -- Backend Module Manager disable icon fix. (by @speedy008) + * [magento/magento2#21301](https://github.com/magento/magento2/pull/21301) -- span tag for more swatches link (by @mageho) + * [magento/magento2#20308](https://github.com/magento/magento2/pull/20308) -- Small PHPDocs fixes [Backend module] (by @SikailoISM) + * [magento/magento2#20617](https://github.com/magento/magento2/pull/20617) -- 14882 product types xml doesn t allow numbers in model instance (by @lisovyievhenii) + * [magento/magento2#21272](https://github.com/magento/magento2/pull/21272) -- Fixed address book display horizontal scroll in responsive view (by @mage2pratik) + * [magento/magento2#19395](https://github.com/magento/magento2/pull/19395) -- store_view_code-column-has-empty-values-in-csv-17784. (by @Valant13) + * [magento/magento2#20071](https://github.com/magento/magento2/pull/20071) -- [Forwardport] [Backport] fixed store wise product filter issue (by @shikhamis11) + * [magento/magento2#20856](https://github.com/magento/magento2/pull/20856) -- ipad-view-order-summary-block (by @dipti2jcommerce) + * [magento/magento2#21298](https://github.com/magento/magento2/pull/21298) -- Fixed pagination drop-down size does not appropriate. (by @mage2pratik) + * [magento/magento2#21310](https://github.com/magento/magento2/pull/21310) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#20371](https://github.com/magento/magento2/pull/20371) -- Issue Fixed: #8086: Multiline admin field is broken (by @vivekkumarcedcoss) + * [magento/magento2#20621](https://github.com/magento/magento2/pull/20621) -- Fix referenced to "store", changing to "scope" in Framework/Mail components (by @gwharton) + * [magento/magento2#20596](https://github.com/magento/magento2/pull/20596) -- Optimize snail_case replacement to PascalCase (by @lbajsarowicz) + * [magento/magento2#21020](https://github.com/magento/magento2/pull/21020) -- Make the module list more deterministic (by @ajardin) + * [magento/magento2#18503](https://github.com/magento/magento2/pull/18503) -- Checkout - Fix JS error Cannot read property 'quoteData' of undefined (by @ihor-sviziev) + * [magento/magento2#19988](https://github.com/magento/magento2/pull/19988) -- Fix for issue 19983 Can't upload customer Image attribute programmatically (by @Nazar65) + * [magento/magento2#20043](https://github.com/magento/magento2/pull/20043) -- Make it possible to generate sales PDF's using the API (by @AntonEvers) + * [magento/magento2#20307](https://github.com/magento/magento2/pull/20307) -- Fixed issue #20305 Update button on payment checkout is not proper alligned (by @GovindaSharma) + * [magento/magento2#20583](https://github.com/magento/magento2/pull/20583) -- 13982 customer login block sets the title for the page when rendered (by @lisovyievhenii) + * [magento/magento2#20950](https://github.com/magento/magento2/pull/20950) -- magento/magento2#20773: Do not throw exception during autoload (by @Vinai) + * [magento/magento2#21045](https://github.com/magento/magento2/pull/21045) -- Update static block in nginx.conf.sample (by @jaideepghosh) + * [magento/magento2#21328](https://github.com/magento/magento2/pull/21328) -- Issue Fixed #21322 : Declarative schema: Omitting indexType throws exception (by @milindsingh) + * [magento/magento2#21335](https://github.com/magento/magento2/pull/21335) -- Fixed #15059 Cannot reorder from the first try (by @shikhamis11) + * [magento/magento2#21347](https://github.com/magento/magento2/pull/21347) -- Applied PHP-CS-Fixer for code cleanup. (by @yogeshsuhagiya) + * [magento/magento2#21360](https://github.com/magento/magento2/pull/21360) -- Solve #21359 Search with long string display horizontal scroll in front end (by @mageprince) + * [magento/magento2#21368](https://github.com/magento/magento2/pull/21368) -- Css property name issue (by @amol2jcommerce) + * [magento/magento2#20044](https://github.com/magento/magento2/pull/20044) -- Sitemap filename can't exceed 32 characters #13937 (by @irajneeshgupta) + * [magento/magento2#20339](https://github.com/magento/magento2/pull/20339) -- issue fixed #20337 Option Title breaking in two line because applying... (by @cedarvinda) + * [magento/magento2#20578](https://github.com/magento/magento2/pull/20578) -- Added original exception as the cause to the new exception on product delete error (by @woutersamaey) + * [magento/magento2#20858](https://github.com/magento/magento2/pull/20858) -- Update details.phtml (by @mageho) + * [magento/magento2#21105](https://github.com/magento/magento2/pull/21105) -- Fixed pagination issue in admin review grid (by @dominicfernando) + * [magento/magento2#21295](https://github.com/magento/magento2/pull/21295) -- Fix empty cart validation (by @wojtekn) + * [magento/magento2#21302](https://github.com/magento/magento2/pull/21302) -- Misconfigured aria-labelledby for product tabs (by @mageho) + * [magento/magento2#21330](https://github.com/magento/magento2/pull/21330) -- Change comment to "database" (by @DanielRuf) + * [magento/magento2#21395](https://github.com/magento/magento2/pull/21395) -- As low as displays incorrect pricing on category page, tax appears to be added twice #21383 (by @Jitheesh) + * [magento/magento2#21401](https://github.com/magento/magento2/pull/21401) -- Show error message when customer click on Add to cart button without selecting atleast one product from recently orderred list (by @mageprince) + * [magento/magento2#21405](https://github.com/magento/magento2/pull/21405) -- Removed unused else block and corrected return types (by @yogeshsuhagiya) + * [magento/magento2#21429](https://github.com/magento/magento2/pull/21429) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#21426](https://github.com/magento/magento2/pull/21426) -- fixes-for-product-page-product-in-website-multi-store-view-not-displa... (by @priti2jcommerce) + * [magento/magento2#21431](https://github.com/magento/magento2/pull/21431) -- Fix grammar (by @DanielRuf) + * [magento/magento2#21151](https://github.com/magento/magento2/pull/21151) -- Database Rollback not working M2.3.0 (by @Stepa4man) + * [magento/magento2#21458](https://github.com/magento/magento2/pull/21458) -- Elasticsearch6 implementation. (by @romainruaud) + * [magento/magento2#20316](https://github.com/magento/magento2/pull/20316) -- Change product_price_value in cart data section based on tax settings (by @NickdeK) + * [magento/magento2#20482](https://github.com/magento/magento2/pull/20482) -- [TASK] Remove translation of attribute store label in getAdditionalData (by @c-walter) + * [magento/magento2#21094](https://github.com/magento/magento2/pull/21094) -- Also populate the storesCache when importing product only on storevie… (by @hostep) + * [magento/magento2#21130](https://github.com/magento/magento2/pull/21130) -- Remove unused use statement in Wishlist Allcart Controller (by @oshancp) + * [magento/magento2#21275](https://github.com/magento/magento2/pull/21275) -- Static tests: forbid 'or' instead of '||' #21062. (by @novikor) + * [magento/magento2#21338](https://github.com/magento/magento2/pull/21338) -- [Catalog] [MediaStorage] Fix watermark in media application (by @progreg) + * [magento/magento2#21363](https://github.com/magento/magento2/pull/21363) -- [Catalog] Fixing the Products grid with default values on multi stores (by @eduard13) + * [magento/magento2#21356](https://github.com/magento/magento2/pull/21356) -- Checkout Page Cancel button is not working #21327 (by @Jitheesh) + * [magento/magento2#21443](https://github.com/magento/magento2/pull/21443) -- Fixed #21425 Date design change show not correctly value in backend (by @shikhamis11) + * [magento/magento2#21474](https://github.com/magento/magento2/pull/21474) -- Refactoring the Form class (by @eduard13) + * [magento/magento2#20079](https://github.com/magento/magento2/pull/20079) -- [2.3] Add support for validation message callback (by @floorz) + * [magento/magento2#20129](https://github.com/magento/magento2/pull/20129) -- Issue fixed #20128 : Date range returns the same start and end date (by @milindsingh) + * [magento/magento2#20818](https://github.com/magento/magento2/pull/20818) -- 14857: prevent cache drop for frontend caches on sitemap generation (by @david-fuehr) + * [magento/magento2#21079](https://github.com/magento/magento2/pull/21079) -- Fixes for product tabbing issue (by @prakash2jcommerce) + * [magento/magento2#21303](https://github.com/magento/magento2/pull/21303) -- Fix-issue-21292 Google Analytics isAnonymizedIpActive always true (by @Nazar65) + * [magento/magento2#21455](https://github.com/magento/magento2/pull/21455) -- Infinite redirects in Magento admin #21454 (by @Jitheesh) + * [magento/magento2#17668](https://github.com/magento/magento2/pull/17668) -- Adding property mapper for product eav attribute -> search weight. (by @bartoszkubicki) + * [magento/magento2#18705](https://github.com/magento/magento2/pull/18705) -- Correct child node load when multiple calls to CategoryManagement::ge… (by @pmclain) + * [magento/magento2#19637](https://github.com/magento/magento2/pull/19637) -- Fixed Issue #19632 - Backend Marketing Cart Price Rule Label Alignment Issue (by @speedy008) + * [magento/magento2#20239](https://github.com/magento/magento2/pull/20239) -- Fixed issue #20187 Downloadble Price duplicate issue (by @GovindaSharma) + * [magento/magento2#20484](https://github.com/magento/magento2/pull/20484) -- Fix performance leak in salesrule collection (by @david-fuehr) + * [magento/magento2#21170](https://github.com/magento/magento2/pull/21170) -- Fix issue with custom option file uploading (by @nikolaevas) + * [magento/magento2#21279](https://github.com/magento/magento2/pull/21279) -- Fixed: #21278, Add sort order on downloadable links (by @maheshWebkul721) + * [magento/magento2#21462](https://github.com/magento/magento2/pull/21462) -- URL rewrite fix while product website update using mass action (by @AnshuMishra17) + * [magento/magento2#21476](https://github.com/magento/magento2/pull/21476) -- Fix/issue 21192 (by @DenisSaltanahmedov) + * [magento/magento2#21503](https://github.com/magento/magento2/pull/21503) -- Remove setting of page title from Form/Register block and add title to customer_account_create layout (by @mfickers) + * [magento/magento2#19376](https://github.com/magento/magento2/pull/19376) -- 19276 - Fixed price renderer issue (by @sarfarazbheda) + * [magento/magento2#20391](https://github.com/magento/magento2/pull/20391) -- Success message is not showing when creating invoice & shipment simultaniously #19942 (by @XxXgeoXxX) + * [magento/magento2#20528](https://github.com/magento/magento2/pull/20528) -- Fix for #20527 [Admin] Configurable product variations table cell labels wrong position (by @vasilii-b) + * [magento/magento2#21498](https://github.com/magento/magento2/pull/21498) -- Setting default sorting #21493 (by @Jitheesh) + * [magento/magento2#21509](https://github.com/magento/magento2/pull/21509) -- Fix: Cart is emptied when enter is pressed after changing product quantity (by @lfluvisotto) + * [magento/magento2#21536](https://github.com/magento/magento2/pull/21536) -- Fix type hints and replace deprecated method usage (by @avstudnitz) + * [magento/magento2#21534](https://github.com/magento/magento2/pull/21534) -- correct spelling (by @ravi-chandra3197) + * [magento/magento2#13184](https://github.com/magento/magento2/pull/13184) -- [FEATURE] added ability to create default/fixed value nodes during XSD Schema Validation (by @matthiasherold) + * [magento/magento2#19635](https://github.com/magento/magento2/pull/19635) -- Patch 18017 magento 23 (by @niravkrish) + * [magento/magento2#20429](https://github.com/magento/magento2/pull/20429) -- 'fixes-for-#20414' :: Recent orders grid not aligned from left in mob… (by @nainesh2jcommerce) + * [magento/magento2#20938](https://github.com/magento/magento2/pull/20938) -- Fixed Massaction design with submenu on grid pages (by @ananth-iyer) + * [magento/magento2#21074](https://github.com/magento/magento2/pull/21074) -- Added space above error message. (by @suryakant-krish) + * [magento/magento2#21371](https://github.com/magento/magento2/pull/21371) -- Fix Admin Customizable Options Dropdown sort_order issue (by @omiroshnichenko) + * [magento/magento2#21420](https://github.com/magento/magento2/pull/21420) -- Wishlist review summary (by @Den4ik) + * [magento/magento2#21542](https://github.com/magento/magento2/pull/21542) -- Remove Environment emulation for better performance on sitemap generation (by @Nazar65) + * [magento/magento2#21611](https://github.com/magento/magento2/pull/21611) -- Advanced-Search-layout-not-proper (by @amol2jcommerce) + * [magento/magento2#21638](https://github.com/magento/magento2/pull/21638) -- Minicart search logo not vertically aligned (by @amol2jcommerce) + * [magento/magento2#21685](https://github.com/magento/magento2/pull/21685) -- Spelling Correction (by @ansari-krish) + * [magento/magento2#21701](https://github.com/magento/magento2/pull/21701) -- Fix Broken Tax Rate Search Filter Admin grid #21521 (by @tuyennn) + * [magento/magento2#21716](https://github.com/magento/magento2/pull/21716) -- [DB] Remove unused variable (by @eduard13) + * [magento/magento2#20001](https://github.com/magento/magento2/pull/20001) -- #13612 Fixed-Quantity_and_stock_status when visibility set to storefront throwing exception (by @aditisinghcedcoss) + * [magento/magento2#21180](https://github.com/magento/magento2/pull/21180) -- [ForwardPort] #18896 Add Mexico Regions (by @osrecio) + * [magento/magento2#21189](https://github.com/magento/magento2/pull/21189) -- Fix/issue 18761 (by @DenisSaltanahmedov) + * [magento/magento2#21468](https://github.com/magento/magento2/pull/21468) -- Fix long string display horizontal scroll in all pages in admin (by @mageprince) + * [magento/magento2#21444](https://github.com/magento/magento2/pull/21444) -- Disable dropdown in JavaScript and CSS Settings in developer configuration (by @ananth-iyer) + * [magento/magento2#21600](https://github.com/magento/magento2/pull/21600) -- Fixed typo mistake (by @yogeshsuhagiya) + * [magento/magento2#21582](https://github.com/magento/magento2/pull/21582) -- Fixed Whitespace issues for related, cross and upsell grids (by @amol2jcommerce) + * [magento/magento2#21683](https://github.com/magento/magento2/pull/21683) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#21731](https://github.com/magento/magento2/pull/21731) -- Fixed wrong proxing in the inventory observer (by @VitaliyBoyko) + * [magento/magento2#21740](https://github.com/magento/magento2/pull/21740) -- Removed extra whitespaces (by @yogeshsuhagiya) + * [magento/magento2#19859](https://github.com/magento/magento2/pull/19859) -- MUI controller lacks JSON response, instead returns status 200 with empty body (by @woutersamaey) + * [magento/magento2#20512](https://github.com/magento/magento2/pull/20512) -- Sorting by Websites not working in product grid in backoffice #20511 (by @XxXgeoXxX) + * [magento/magento2#20785](https://github.com/magento/magento2/pull/20785) -- [Forwardport] Use batches and direct queries to fix sales address upgrade (by @ihor-sviziev) + * [magento/magento2#20840](https://github.com/magento/magento2/pull/20840) -- Missed form validation in Admin Order Address Edit route sales/order/address (by @XxXgeoXxX) + * [magento/magento2#21713](https://github.com/magento/magento2/pull/21713) -- Resolve Issue : Search REST API returns wrong total_count (by @ronak2ram) + * [magento/magento2#18633](https://github.com/magento/magento2/pull/18633) -- Fix for issue magento/magento2#18630 (by @dverkade) + * [magento/magento2#21649](https://github.com/magento/magento2/pull/21649) -- Fix #21648 Checkout Agreements checkbox missing asterisk (by @Karlasa) + * [magento/magento2#21782](https://github.com/magento/magento2/pull/21782) -- Edited headings to be more consistent (by @mikeshatch) + * [magento/magento2#21288](https://github.com/magento/magento2/pull/21288) -- magento/magento2#12396: Total Amount cart rule without tax (by @AleksLi) + * [magento/magento2#21469](https://github.com/magento/magento2/pull/21469) -- Update price-bundle.js so that correct tier price is calculated while displaying in bundle product (by @adarshkhatri) + * [magento/magento2#21575](https://github.com/magento/magento2/pull/21575) -- Fix for issue #21510: Can't access backend indexers page after creating a custom index (by @ccasciotti) + * [magento/magento2#21751](https://github.com/magento/magento2/pull/21751) -- fix #21750 remove translation of product attribute label (by @Karlasa) + * [magento/magento2#21774](https://github.com/magento/magento2/pull/21774) -- MSI: Add deprecation message to CatalogInventory SPIs (by @lbajsarowicz) + * [magento/magento2#21785](https://github.com/magento/magento2/pull/21785) -- [Forwardport] [Checkout] Fix clearing admin quote address when removing all items (by @eduard13) + * [magento/magento2#21795](https://github.com/magento/magento2/pull/21795) -- [Wishlist] Covering the Wishlist classes by integration and unit tests (by @eduard13) + * [magento/magento2#21791](https://github.com/magento/magento2/pull/21791) -- #19835 Fix admin header buttons flicker (by @OlehWolf) + * [magento/magento2#21826](https://github.com/magento/magento2/pull/21826) -- Flying Fists of Kung Fu Cleanup (by @lefte) + * [magento/magento2#21376](https://github.com/magento/magento2/pull/21376) -- Fixed Inline block edit identifier validation (by @niravkrish) + * [magento/magento2#21399](https://github.com/magento/magento2/pull/21399) -- Fixed : Additional addresses DataTable Pagination count displaying wrong (by @Dharmeshvaja91) + * [magento/magento2#21693](https://github.com/magento/magento2/pull/21693) -- Fix #21692 #21752 - logic in constructor of address validator and Locale Resolver check (by @Bartlomiejsz) + * [magento/magento2#21815](https://github.com/magento/magento2/pull/21815) -- Fill data_hash from BULK response with correct data (by @silyadev) + * [magento/magento2#21820](https://github.com/magento/magento2/pull/21820) -- #20825 Missing required argument $productAvailabilityChecks of Magent... (by @kisroman) + * [magento/magento2#21843](https://github.com/magento/magento2/pull/21843) -- Fix typo (by @nasanabri) + * [magento/magento2#21851](https://github.com/magento/magento2/pull/21851) -- [Frontend] Fixing the accessibility standards violation (by @eduard13) + * [magento/magento2#21884](https://github.com/magento/magento2/pull/21884) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#19727](https://github.com/magento/magento2/pull/19727) -- Use repository to load order when manually creating an invoice (by @JeroenVanLeusden) + * [magento/magento2#20212](https://github.com/magento/magento2/pull/20212) -- Secure errors directory (by @schmengler) + * [magento/magento2#20774](https://github.com/magento/magento2/pull/20774) -- 20434 consider url rewrite when change product visibility attribute 2 3 (by @VitaliyBoyko) + * [magento/magento2#21283](https://github.com/magento/magento2/pull/21283) -- Fixed calculation of 'Total' column under "Last Orders" listing on the admin dashboard (by @rav-redchamps) + * [magento/magento2#21621](https://github.com/magento/magento2/pull/21621) -- Updated review text in admin menu (by @gelanivishal) + * [magento/magento2#21778](https://github.com/magento/magento2/pull/21778) -- Multishipping checkout agreements now are the same as default checkout agreements (by @samuel27m) + * [magento/magento2#21825](https://github.com/magento/magento2/pull/21825) -- When setting `background` for labels explicitly the labels in admin will (by @TomashKhamlai) + * [magento/magento2#21880](https://github.com/magento/magento2/pull/21880) -- magento/magento2#21001 - fix unit tests, by passing currency to numbe… (by @kdegorski) + * [magento/magento2#21899](https://github.com/magento/magento2/pull/21899) -- Trigger contentUpdate on reviews load (by @jahvi) + * [magento/magento2#19871](https://github.com/magento/magento2/pull/19871) -- Added custom_options file upload directory to .gitignore. (by @erfanimani) + * [magento/magento2#21697](https://github.com/magento/magento2/pull/21697) -- Root exception not logged on QuoteManagement::submitQuote (by @david-fuehr) + * [magento/magento2#21776](https://github.com/magento/magento2/pull/21776) -- #21734 Error in JS validation rule (by @kisroman) + * [magento/magento2#21822](https://github.com/magento/magento2/pull/21822) -- Refactor \Order\Shipment\AddTrack Controller to use ResultInterface (by @JeroenVanLeusden) + * [magento/magento2#21919](https://github.com/magento/magento2/pull/21919) -- Fix gallery full-screen triggers (by @iGerchak) + * [magento/magento2#21921](https://github.com/magento/magento2/pull/21921) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#18067](https://github.com/magento/magento2/pull/18067) -- Fixes variables in configuration fields not being replaced with actual value… (by @hostep) + * [magento/magento2#19265](https://github.com/magento/magento2/pull/19265) -- add missing fields to quote_address (by @ErikPel) + * [magento/magento2#20526](https://github.com/magento/magento2/pull/20526) -- [EAV] Improving the EAV attribute code validation, by not allowing to use n... (by @eduard13) + * [magento/magento2#20898](https://github.com/magento/magento2/pull/20898) -- Fixed #13319 , Incorrect method return value in \Magento\Shipping\Model\Carrier\AbstractCarrier::getTotalNumOfBoxes() (by @cedmudit) + * [magento/magento2#21053](https://github.com/magento/magento2/pull/21053) -- Allow redis compression options to be specified during `setup:install` process (by @cmacdonald-au) + * [magento/magento2#21065](https://github.com/magento/magento2/pull/21065) -- Refactored Retrieval Of Entity ID To Make AbstractDataProvider Usable (by @sprankhub) + * [magento/magento2#21135](https://github.com/magento/magento2/pull/21135) -- Fix eav form foreach error #21134 (by @wojtekn) + * [magento/magento2#21465](https://github.com/magento/magento2/pull/21465) -- Module data fixtures for @magentoDataFixtureBeforeTransaction annotations (by @Vinai) + * [magento/magento2#21484](https://github.com/magento/magento2/pull/21484) -- Populate label elements for street address fields in checkout (by @scottsb) + * [magento/magento2#21511](https://github.com/magento/magento2/pull/21511) -- SHQ18-1568 Updating UPS endpoint to use https. Http is no longer reli… (by @wsajosh) + * [magento/magento2#21749](https://github.com/magento/magento2/pull/21749) -- Sitemap Generation - Product URL check null fix (by @asim-vax) + * [magento/magento2#21834](https://github.com/magento/magento2/pull/21834) -- Add argument to show filter text in URL rewrite grid after click on back button (by @vbmagento) + * [magento/magento2#18386](https://github.com/magento/magento2/pull/18386) -- Cleaner documentation for Travis CI static tests (by @Thundar) + * [magento/magento2#19765](https://github.com/magento/magento2/pull/19765) -- Resolved undefined index issue for import adapter (by @jaimin-ktpl) + * [magento/magento2#21216](https://github.com/magento/magento2/pull/21216) -- Elasticsearch price fieldname is incorrect during indexing when storeId and websiteId do not match (by @alexander-aleman) + * [magento/magento2#21767](https://github.com/magento/magento2/pull/21767) -- Magento should create a log entry if an observer does not implement ObserverInterface (by @Nazar65) + * [magento/magento2#21896](https://github.com/magento/magento2/pull/21896) -- Contact us layout in I-pad not proper (by @amol2jcommerce) + * [magento/magento2#21927](https://github.com/magento/magento2/pull/21927) -- Removing obsolete non-English translation files, these aren't transla… (by @hostep) + * [magento/magento2#21940](https://github.com/magento/magento2/pull/21940) -- [UI] Adjusting the Magento_Ui typos (by @eduard13) + * [magento/magento2#22026](https://github.com/magento/magento2/pull/22026) -- Removed two time zlib.output_compression on section (by @samuel20miglia) + * [magento/magento2#22055](https://github.com/magento/magento2/pull/22055) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#22056](https://github.com/magento/magento2/pull/22056) -- Spelling Correction (by @ansari-krish) + * [magento/magento2#22075](https://github.com/magento/magento2/pull/22075) -- Add space after asterisk to show as list (by @likemusic) + * [magento/magento2#18440](https://github.com/magento/magento2/pull/18440) -- [2.3] Reworked gallery.phtml to move generation of gallery json strings to own block functions (by @gwharton) + * [magento/magento2#18933](https://github.com/magento/magento2/pull/18933) -- Update typeReferenceBlock definition (by @leandrommedeiros) + * [magento/magento2#19987](https://github.com/magento/magento2/pull/19987) -- Fix broken widget placeholders after upgrading from 2.2 (by @vovayatsyuk) + * [magento/magento2#21008](https://github.com/magento/magento2/pull/21008) -- [Widget] Fixing the multidimensional array as value for the widget's parameter (by @eduard13) + * [magento/magento2#21545](https://github.com/magento/magento2/pull/21545) -- Ensure `__toString()` catches all error types (by @tylerssn) + * [magento/magento2#21647](https://github.com/magento/magento2/pull/21647) -- MFTF / Remove redundant ActionGroups (by @lbajsarowicz) + * [magento/magento2#21754](https://github.com/magento/magento2/pull/21754) -- Fixes nested array for used products cache key (by @michaellehmkuhl) + * [magento/magento2#21818](https://github.com/magento/magento2/pull/21818) -- Remove all marketing get params on Varnish to minimize the cache objects (by @ihor-sviziev) + * [magento/magento2#21928](https://github.com/magento/magento2/pull/21928) -- Set minimum qty 1 after cast to int (by @likemusic) + * [magento/magento2#21948](https://github.com/magento/magento2/pull/21948) -- Fixed WhiteSpace issue in product grid (by @shrinet) + * [magento/magento2#21966](https://github.com/magento/magento2/pull/21966) -- Remove timestap from current date when saving product special price from date (by @JeroenVanLeusden) + * [magento/magento2#22054](https://github.com/magento/magento2/pull/22054) -- fix strpos args order (by @quasilyte) + * [magento/magento2#20951](https://github.com/magento/magento2/pull/20951) -- Direct STDERR output when listing crontab to /dev/null (by @danielatdattrixdotcom) + * [magento/magento2#21023](https://github.com/magento/magento2/pull/21023) -- Corrected the translation for comment tag (by @yogeshsuhagiya) + * [magento/magento2#21790](https://github.com/magento/magento2/pull/21790) -- #21789 Fix gallery event observer (by @Den4ik) + * [magento/magento2#21999](https://github.com/magento/magento2/pull/21999) -- #21998 Magento/ImportExport/Model/Import has _coreConfig declared dyn… (by @kisroman) + * [magento/magento2#22012](https://github.com/magento/magento2/pull/22012) -- Correct bug 21993 config:set not storing scoped values (by @ochnygosch) + * [magento/magento2#22081](https://github.com/magento/magento2/pull/22081) -- phpcs error on rule classes - must be of the type integer (by @Nazar65) + * [magento/magento2#21083](https://github.com/magento/magento2/pull/21083) -- Turn on edit mode for product repository when adding children (by @pedrosousa13) + * [magento/magento2#21540](https://github.com/magento/magento2/pull/21540) -- Move Magento\Framework\HTTP\ClientInterface preference to app/etc/di.xml (by @kassner) + * [magento/magento2#21932](https://github.com/magento/magento2/pull/21932) -- Admin-Order-Create-Set-Save-address-checkbox-true-as-default-#106 (by @krnshah) + * [magento/magento2#22002](https://github.com/magento/magento2/pull/22002) -- Removed unwanted interface implementation (by @vishal-7037) + * [magento/magento2#22135](https://github.com/magento/magento2/pull/22135) -- Fix broken link in README.md (by @samuel27m) + * [magento/magento2#21821](https://github.com/magento/magento2/pull/21821) -- Aligning tooltip action on dashboard (by @rafaelstz) + * [magento/magento2#22046](https://github.com/magento/magento2/pull/22046) -- FIX for issue #21916 - Elasticsearch6 generation does not exist (by @phoenix128) + * [magento/magento2#22091](https://github.com/magento/magento2/pull/22091) -- Fixed assignment of the guest customer to the guest group when 'Automatic Assignment by VAT ID' is enabled (by @vovayatsyuk) + * [magento/magento2#22117](https://github.com/magento/magento2/pull/22117) -- Previous scrolling to invalid form element is not being canceled on h… (by @yvechirko) + * [magento/magento2#22128](https://github.com/magento/magento2/pull/22128) -- Fixes issue - #21824. "save_parameters_in_session" set to true for admin user grid (by @jayankaghosh) + * [magento/magento2#22151](https://github.com/magento/magento2/pull/22151) -- Update PatchApplierTest.php - Corrected Spelling (by @ryantfowler) + * [magento/magento2#22171](https://github.com/magento/magento2/pull/22171) -- unregister phar only when appropriate (by @adaudenthun) + * [magento/magento2#22184](https://github.com/magento/magento2/pull/22184) -- Removing incorrect less selector '.abs-cleafix', it has a typo + it i… (by @hostep) + * [magento/magento2#22195](https://github.com/magento/magento2/pull/22195) -- 22166: updated README to follow-up the switch from Bugcrowd to hackerone (by @mautz-et-tong) + * [magento/magento2#22201](https://github.com/magento/magento2/pull/22201) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#22205](https://github.com/magento/magento2/pull/22205) -- Translated exception message (by @yogeshsuhagiya) + * [magento/magento2#22207](https://github.com/magento/magento2/pull/22207) -- Translate comment tag in DHL config settings (by @yogeshsuhagiya) + * [magento/magento2#22210](https://github.com/magento/magento2/pull/22210) -- Typography_change (by @krnshah) + * [magento/magento2#22239](https://github.com/magento/magento2/pull/22239) -- Spelling Correction (by @ansari-krish) + * [magento/magento2#22258](https://github.com/magento/magento2/pull/22258) -- Use proper variables for tooltip styles on tablet devices (by @vovayatsyuk) + * [magento/magento2#19530](https://github.com/magento/magento2/pull/19530) -- Fixed fatal error if upgrading from Magento v2.0.0 to v2.3 and non system attributes missing (by @suneet64) + * [magento/magento2#20182](https://github.com/magento/magento2/pull/20182) -- Use correct base path to check if setup folder exists (by @JeroenVanLeusden) + * [magento/magento2#20832](https://github.com/magento/magento2/pull/20832) -- fixes-for-customer-name-twice-desktop (by @priti2jcommerce) + * [magento/magento2#21501](https://github.com/magento/magento2/pull/21501) -- Same product quantity not increment when added with guest user. #21375 (by @Jitheesh) + * [magento/magento2#21788](https://github.com/magento/magento2/pull/21788) -- #21786 Fixed asynchronous email sending for the sales entities which were created with disabled email sending (by @serhiyzhovnir) + * [magento/magento2#22073](https://github.com/magento/magento2/pull/22073) -- Bug fix for #21753 (2.3-develop) (by @crankycyclops) + * [magento/magento2#22220](https://github.com/magento/magento2/pull/22220) -- Remove an unused variable from order_list fixture in the integration test suite. (by @evktalo) + * [magento/magento2#20295](https://github.com/magento/magento2/pull/20295) -- Small PHPDocs fixes (by @SikailoISM) + * [magento/magento2#20378](https://github.com/magento/magento2/pull/20378) -- 12386: Order Status resets to default Status after Partial Refund. (by @nmalevanec) + * [magento/magento2#20772](https://github.com/magento/magento2/pull/20772) -- Fixed inactive admin user token (by @mage2pratik) + * [magento/magento2#20968](https://github.com/magento/magento2/pull/20968) -- Remove direct $_SERVER variable use (by @dominicfernando) + * [magento/magento2#21869](https://github.com/magento/magento2/pull/21869) -- Fix importFromArray by setting _isCollectionLoaded to true after import (by @slackerzz) + * [magento/magento2#22031](https://github.com/magento/magento2/pull/22031) -- Fixed typo error in sales grid at admin (by @vishal-7037) + * [magento/magento2#22160](https://github.com/magento/magento2/pull/22160) -- Remove duplicate styling (by @arnoudhgz) + * [magento/magento2#22197](https://github.com/magento/magento2/pull/22197) -- Fix > Exception #0 (BadMethodCallException): Missing required argument $msrpPriceCalculators of Magento\Msrp\Pricing\MsrpPriceCalculator. (by @lfluvisotto) + * [magento/magento2#22202](https://github.com/magento/magento2/pull/22202) -- Fixed wrong url redirect when edit product review from product view page (by @ravi-chandra3197) + * [magento/magento2#22340](https://github.com/magento/magento2/pull/22340) -- Fixed Value of created_at and updated_at columns (by @shikhamis11) + * [magento/magento2#22357](https://github.com/magento/magento2/pull/22357) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#21378](https://github.com/magento/magento2/pull/21378) -- Fix for issue #21299. Change HEAD action mapping to GET action interface and add HEAD request handling (by @mattijv) + * [magento/magento2#21936](https://github.com/magento/magento2/pull/21936) -- 21907: Place order button disabled after failed email address validation check with braintree credit card (by @kisroman) + * [magento/magento2#21968](https://github.com/magento/magento2/pull/21968) -- Layered Navigation: “Equalize product count” not working as expected (by @Nazar65) + * [magento/magento2#22133](https://github.com/magento/magento2/pull/22133) -- setAttributeSetFilter accepts both integer and integer-array (by @NiklasBr) + * [magento/magento2#22154](https://github.com/magento/magento2/pull/22154) -- Fiexed 22152 - Click on search icon it does not working on admin grid sticky header (by @niravkrish) + * [magento/magento2#22200](https://github.com/magento/magento2/pull/22200) -- fatalErrorHandler returns 500 only on fatal errors (by @wexo-team) + * [magento/magento2#22226](https://github.com/magento/magento2/pull/22226) -- Remove all marketing get params on Varnish to minimize the cache objects (added facebook and bronto parameter) (by @lfluvisotto) + * [magento/magento2#22265](https://github.com/magento/magento2/pull/22265) -- Show the correct subtotal amount for partial creditmemo email (by @kassner) + * [magento/magento2#22281](https://github.com/magento/magento2/pull/22281) -- Fixed "Please specify the admin custom URL" error on app:config:import CLI command (by @davidalger) + * [magento/magento2#22298](https://github.com/magento/magento2/pull/22298) -- Alignment Issue While Editing Order Data containing Downlodable Products with "Links can be purchased separately" enabled in Admin Section (by @ansari-krish) + * [magento/magento2#22318](https://github.com/magento/magento2/pull/22318) -- #21747 Fix catalog_product_flat_data attribute value for store during indexer (by @OlehWolf) + * [magento/magento2#22320](https://github.com/magento/magento2/pull/22320) -- Removed redundant Gallery subscription for catalog rules (by @VitaliyBoyko) + * [magento/magento2#22321](https://github.com/magento/magento2/pull/22321) -- magento/magento2#22317: CodeSniffer should not mark correctly aligned DocBlock elements as code style violation. (by @p-bystritsky) + * [magento/magento2#22332](https://github.com/magento/magento2/pull/22332) -- Fixes a less compilation error: '.no-link a' isn't defined when .emai… (by @hostep) + * [magento/magento2#22339](https://github.com/magento/magento2/pull/22339) -- Spelling Mistake in Setup > Patch (by @sudhanshu-bajaj) + * [magento/magento2#22362](https://github.com/magento/magento2/pull/22362) -- [Fixed] Category Update without "name" cannot be saved in scope "all" with REST API (by @niravkrish) + * [magento/magento2#22381](https://github.com/magento/magento2/pull/22381) -- Qty box visibility issue in wishlist when product is out of stock (by @ansari-krish) + * [magento/magento2#19993](https://github.com/magento/magento2/pull/19993) -- fixed issue of Backup tool not correctly detecting .maintenance.flag (by @hiren0241) + * [magento/magento2#20174](https://github.com/magento/magento2/pull/20174) -- Fix issues inserting Widgets with nested WYSIWYGs (by @molovo) + * [magento/magento2#21670](https://github.com/magento/magento2/pull/21670) -- Fix getSize method after clearing data collection (by @sergeynezbritskiy) + * [magento/magento2#21711](https://github.com/magento/magento2/pull/21711) -- Purchasing a downloadable product as guest then creating an account on the onepagesuccess step doesn't link product with account (by @Jitheesh) + * [magento/magento2#21756](https://github.com/magento/magento2/pull/21756) -- Fixes race condition when building merged css/js file during simultaneous requests (by @Ian410) + * [magento/magento2#21816](https://github.com/magento/magento2/pull/21816) -- #21779 Adminhtml textarea field doesn't accept maxlength (by @kisroman) + * [magento/magento2#22233](https://github.com/magento/magento2/pull/22233) -- [Fixed] Full Tax Summary missing calculation Admin create order (by @niravkrish) + * [magento/magento2#22263](https://github.com/magento/magento2/pull/22263) -- Prevented /Magento/Sales/Model/Service/InvoiceService.php incorrectly… (by @ryanpalmerweb) + * [magento/magento2#22382](https://github.com/magento/magento2/pull/22382) -- Don't skip row on import if image not available. (by @Nazar65) + * [magento/magento2#22420](https://github.com/magento/magento2/pull/22420) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#22421](https://github.com/magento/magento2/pull/22421) -- Spelling correction (by @ansari-krish) + * [magento/magento2#18336](https://github.com/magento/magento2/pull/18336) -- add more CDATA-related tests for `Magento\Framework\Config\Dom::merge` and fix failing ones (by @enl) + * [magento/magento2#19806](https://github.com/magento/magento2/pull/19806) -- Fixed Changing sample for downloadable product failure (by @ravi-chandra3197) + * [magento/magento2#21772](https://github.com/magento/magento2/pull/21772) -- Make sure Yes/No attribute Layered Navigation filter uses index (by @stkec) + * [magento/magento2#21797](https://github.com/magento/magento2/pull/21797) -- magento/magento2#8035: Join extension attributes are not added to Order results (REST api) (by @swnsma) + * [magento/magento2#21979](https://github.com/magento/magento2/pull/21979) -- [Component Rule] Revert es6 variable declarations (by @Den4ik) + * [magento/magento2#22033](https://github.com/magento/magento2/pull/22033) -- Add multibyte support for attributeSource getOptionId method (by @gomencal) + * [magento/magento2#22082](https://github.com/magento/magento2/pull/22082) -- Remove fotorama.min.js (by @iGerchak) + * [magento/magento2#22089](https://github.com/magento/magento2/pull/22089) -- Fotorama - disabling swipe on the item with class "disableSwipe" (by @iGerchak) + * [magento/magento2#22291](https://github.com/magento/magento2/pull/22291) -- Fixed #22223 Missing/Wrong data display on downloadable report table … (by @shikhamis11) + * [magento/magento2#22364](https://github.com/magento/magento2/pull/22364) -- Fix buttonId for credit memo button on admin invoice view (by @kassner) + * [magento/magento2#21787](https://github.com/magento/magento2/pull/21787) -- Fix for Issue #7227: "x_forwarded_for" value is always empty in Order object (by @cmuench) + * [magento/magento2#22059](https://github.com/magento/magento2/pull/22059) -- #22047 Feature: Newrelic transaction name based on CLI name (by @lbajsarowicz) + * [magento/magento2#22074](https://github.com/magento/magento2/pull/22074) -- Remove @SuppressWarnings and optimize imports on Category View (by @arnoudhgz) + * [magento/magento2#22178](https://github.com/magento/magento2/pull/22178) -- #21737 Duplicating product with translated url keys over multiple sto… (by @yvechirko) + * [magento/magento2#22324](https://github.com/magento/magento2/pull/22324) -- Adding a validation before adding or executing layout generator class. (by @tiagosampaio) + * [magento/magento2#22475](https://github.com/magento/magento2/pull/22475) -- Fixed Dependency on Backup Settings Configuration (by @keyuremipro) + * [magento/magento2#22285](https://github.com/magento/magento2/pull/22285) -- make return_path_email and set_return_path configurable on website and store scope as well (by @mhauri) + * [magento/magento2#22411](https://github.com/magento/magento2/pull/22411) -- Checkout Totals Sort Order fields can't be empty and should be a number (by @barbanet) + * [magento/magento2#22424](https://github.com/magento/magento2/pull/22424) -- PUT /V1/products/:sku/media/:entryId does not change the file (by @Nazar65) + * [magento/magento2#22446](https://github.com/magento/magento2/pull/22446) -- Removes usage of classes which don't exist from DB migration scripts. (by @hostep) + * [magento/magento2#22456](https://github.com/magento/magento2/pull/22456) -- Fixed issue of drop-down arrow direction in cart price rule (by @hiren0241) + * [magento/magento2#22469](https://github.com/magento/magento2/pull/22469) -- Fix #20111 - display variables in popup while editing existing email template (by @Bartlomiejsz) + * [magento/magento2#22470](https://github.com/magento/magento2/pull/22470) -- Correct spelling (by @ravi-chandra3197) + * [magento/magento2#18541](https://github.com/magento/magento2/pull/18541) -- Set view models as shareable by default (by @thomas-kl1) + * [magento/magento2#21150](https://github.com/magento/magento2/pull/21150) -- Can't scroll in modal popup on i os (by @priti2jcommerce) + * [magento/magento2#21963](https://github.com/magento/magento2/pull/21963) -- Fixed shipping method block alignment issue (by @vishal-7037) + * [magento/magento2#22276](https://github.com/magento/magento2/pull/22276) -- only trigger livereload by .css files (by @torhoehn) + * [magento/magento2#22302](https://github.com/magento/magento2/pull/22302) -- #22299: Cms block cache key does not contain the store id (by @tzyganu) + * [magento/magento2#22466](https://github.com/magento/magento2/pull/22466) -- Fix configurable dropdown showing tax incorrectly in 2.3-develop (by @danielpfarmer) + * [magento/magento2#19653](https://github.com/magento/magento2/pull/19653) -- Adding product from wishlist not adding to cart showing warning message. (by @khodu) + * [magento/magento2#20842](https://github.com/magento/magento2/pull/20842) -- REST products update category_ids cannot be removed (by @ygyryn) + * [magento/magento2#21486](https://github.com/magento/magento2/pull/21486) -- Fix for issue #21477 sets CURRENT_TIMESTAMP on updated_at fields (by @dverkade) + * [magento/magento2#21549](https://github.com/magento/magento2/pull/21549) -- Fixed curl adapter to properly set http version based on $http_ver argument (by @davidalger) + * [magento/magento2#21633](https://github.com/magento/magento2/pull/21633) -- [Forwardport] Resolve incorrect scope code selection when the requested scopeCode is null (by @mage2pratik) + * [magento/magento2#19913](https://github.com/magento/magento2/pull/19913) -- [#19908] locale in rest calls is always default locale (by @jwundrak) + * [magento/magento2#21856](https://github.com/magento/magento2/pull/21856) -- 21842: don't cache absolute file paths in validator factory (by @david-fuehr) + * [magento/magento2#22147](https://github.com/magento/magento2/pull/22147) -- Fixed #22052 Customer account confirmation is overwritten by backend customer save (by @shikhamis11) + * [magento/magento2#22132](https://github.com/magento/magento2/pull/22132) -- Error on design configuration save with imageUploader form element po… (by @yvechirko) + * [magento/magento2#22149](https://github.com/magento/magento2/pull/22149) -- Fix #12802 - allow to override preference over CartInterface and return correct object from QuoteRepository (by @Bartlomiejsz) + * [magento/magento2#22230](https://github.com/magento/magento2/pull/22230) -- Shortening currency list in Configuration->General (replace PR #20397) (by @melaxon) + * [magento/magento2#22399](https://github.com/magento/magento2/pull/22399) -- Fix the invalid currency error in credit card payment of PayPal Payflow Pro or Payments Pro (by @Hailong) + * [magento/magento2#22405](https://github.com/magento/magento2/pull/22405) -- [Fixed] Checkout Section: Shipping step is getting skipped when customer hitting direct payment step URL (by @niravkrish) + 2.3.0 ============= To get detailed information about changes in Magento 2.3.0, see the [Release Notes](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html) +2.2.0 +============= +To get detailed information about changes in Magento 2.2.0, see the [Release Notes](https://devdocs.magento.com/guides/v2.2/release-notes/bk-release-notes.html) + 2.1.0 ============= To get detailed information about changes in Magento 2.1.0, please visit [Magento Community Edition (CE) Release Notes](https://devdocs.magento.com/guides/v2.1/release-notes/ReleaseNotes2.1.0CE.html "Magento Community Edition (CE) Release Notes") diff --git a/app/code/Magento/Authorization/Model/Role.php b/app/code/Magento/Authorization/Model/Role.php index dcc46ee77ee12..fc32fbcaa2e98 100644 --- a/app/code/Magento/Authorization/Model/Role.php +++ b/app/code/Magento/Authorization/Model/Role.php @@ -40,7 +40,7 @@ class Role extends \Magento\Framework\Model\AbstractModel * @param \Magento\Authorization\Model\ResourceModel\Role\Collection $resourceCollection * @param array $data */ - public function __construct( + public function __construct( //phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod \Magento\Framework\Model\Context $context, \Magento\Framework\Registry $registry, \Magento\Authorization\Model\ResourceModel\Role $resource, @@ -52,28 +52,18 @@ public function __construct( /** * @inheritDoc - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $properties = parent::__sleep(); return array_diff($properties, ['_resource', '_resourceCollection']); } /** * @inheritDoc - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_resource = $objectManager->get(\Magento\Authorization\Model\ResourceModel\Role::class); diff --git a/app/code/Magento/Authorizenet/Test/Mftf/Test/StorefrontVerifySecureURLRedirectAuthorizenetTest.xml b/app/code/Magento/Authorizenet/Test/Mftf/Test/StorefrontVerifySecureURLRedirectAuthorizenetTest.xml new file mode 100644 index 0000000000000..5db903f0ed54a --- /dev/null +++ b/app/code/Magento/Authorizenet/Test/Mftf/Test/StorefrontVerifySecureURLRedirectAuthorizenetTest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + <description value="Verify that the Secure URL configuration applies to the Authorizenet pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15610"/> + <group value="authorizenet"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/authorizenet" stepKey="goToUnsecureAuthorizenetURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/authorizenet" stepKey="seeSecureAuthorizenetURL"/> + </test> +</tests> diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js index 68c2f22f6ed44..e3a0886797d63 100644 --- a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js @@ -8,9 +8,8 @@ define([ ], function (AuthorizenetAcceptjs, $) { 'use strict'; - return function (data, element) { - var $form = $(element), - config = data.config; + return function (config, element) { + var $form = $(element); config.active = $form.length > 0 && !$form.is(':hidden'); new AuthorizenetAcceptjs(config); diff --git a/app/code/Magento/Backend/Block/Context.php b/app/code/Magento/Backend/Block/Context.php index d05cdc5fff5a3..d95a76abe2630 100644 --- a/app/code/Magento/Backend/Block/Context.php +++ b/app/code/Magento/Backend/Block/Context.php @@ -5,6 +5,8 @@ */ namespace Magento\Backend\Block; +use Magento\Framework\Cache\LockGuardedCacheLoader; + /** * Constructor modification point for Magento\Backend\Block\AbstractBlock. * @@ -17,7 +19,7 @@ * the classes they were introduced for. * * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD) * @since 100.0.2 */ class Context extends \Magento\Framework\View\Element\Context @@ -44,8 +46,9 @@ class Context extends \Magento\Framework\View\Element\Context * @param \Magento\Framework\Escaper $escaper * @param \Magento\Framework\Filter\FilterManager $filterManager * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Framework\AuthorizationInterface $authorization * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation + * @param \Magento\Framework\AuthorizationInterface $authorization + * @param LockGuardedCacheLoader|null $lockQuery * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -67,7 +70,8 @@ public function __construct( \Magento\Framework\Filter\FilterManager $filterManager, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, - \Magento\Framework\AuthorizationInterface $authorization + \Magento\Framework\AuthorizationInterface $authorization, + LockGuardedCacheLoader $lockQuery = null ) { $this->_authorization = $authorization; parent::__construct( @@ -87,7 +91,8 @@ public function __construct( $escaper, $filterManager, $localeDate, - $inlineTranslation + $inlineTranslation, + $lockQuery ); } diff --git a/app/code/Magento/Backend/Block/System/Design/Edit.php b/app/code/Magento/Backend/Block/System/Design/Edit.php index 4d6c26e4cfe4b..a0cbe4b640d97 100644 --- a/app/code/Magento/Backend/Block/System/Design/Edit.php +++ b/app/code/Magento/Backend/Block/System/Design/Edit.php @@ -5,6 +5,9 @@ */ namespace Magento\Backend\Block\System\Design; +/** + * Edit store design schedule block. + */ class Edit extends \Magento\Backend\Block\Widget { /** @@ -20,6 +23,8 @@ class Edit extends \Magento\Backend\Block\Widget protected $_coreRegistry = null; /** + * @inheritdoc + * * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry * @param array $data @@ -34,6 +39,8 @@ public function __construct( } /** + * @inheritdoc + * * @return void */ protected function _construct() @@ -44,7 +51,7 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareLayout() { @@ -66,7 +73,7 @@ protected function _prepareLayout() 'label' => __('Delete'), 'onclick' => 'deleteConfirm(\'' . __( 'Are you sure?' - ) . '\', \'' . $this->getDeleteUrl() . '\')', + ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'class' => 'delete' ] ); @@ -88,6 +95,8 @@ protected function _prepareLayout() } /** + * Return design change Id. + * * @return string */ public function getDesignChangeId() @@ -96,6 +105,8 @@ public function getDesignChangeId() } /** + * Return delete url. + * * @return string */ public function getDeleteUrl() @@ -104,6 +115,8 @@ public function getDeleteUrl() } /** + * Return save url for edit form. + * * @return string */ public function getSaveUrl() @@ -112,6 +125,8 @@ public function getSaveUrl() } /** + * Return validation url for edit form. + * * @return string */ public function getValidationUrl() @@ -120,6 +135,8 @@ public function getValidationUrl() } /** + * Return page header. + * * @return string */ public function getHeader() diff --git a/app/code/Magento/Backend/Block/Template/Context.php b/app/code/Magento/Backend/Block/Template/Context.php index 27c777c6d4009..6554df88a753c 100644 --- a/app/code/Magento/Backend/Block/Template/Context.php +++ b/app/code/Magento/Backend/Block/Template/Context.php @@ -5,6 +5,8 @@ */ namespace Magento\Backend\Block\Template; +use Magento\Framework\Cache\LockGuardedCacheLoader; + /** * Constructor modification point for Magento\Backend\Block\Template. * @@ -85,6 +87,7 @@ class Context extends \Magento\Framework\View\Element\Template\Context * @param \Magento\Framework\Math\Random $mathRandom * @param \Magento\Framework\Data\Form\FormKey $formKey * @param \Magento\Framework\Code\NameBuilder $nameBuilder + * @param LockGuardedCacheLoader|null $lockQuery * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -118,7 +121,8 @@ public function __construct( \Magento\Backend\Model\Session $backendSession, \Magento\Framework\Math\Random $mathRandom, \Magento\Framework\Data\Form\FormKey $formKey, - \Magento\Framework\Code\NameBuilder $nameBuilder + \Magento\Framework\Code\NameBuilder $nameBuilder, + LockGuardedCacheLoader $lockQuery = null ) { $this->_authorization = $authorization; $this->_backendSession = $backendSession; @@ -150,7 +154,8 @@ public function __construct( $storeManager, $pageConfig, $resolver, - $validator + $validator, + $lockQuery ); } diff --git a/app/code/Magento/Backend/Block/Widget/Context.php b/app/code/Magento/Backend/Block/Widget/Context.php index bfeb86214d33e..69374979b3fe5 100644 --- a/app/code/Magento/Backend/Block/Widget/Context.php +++ b/app/code/Magento/Backend/Block/Widget/Context.php @@ -5,6 +5,8 @@ */ namespace Magento\Backend\Block\Widget; +use Magento\Framework\Cache\LockGuardedCacheLoader; + /** * Constructor modification point for Magento\Backend\Block\Widget. * @@ -17,7 +19,7 @@ * the classes they were introduced for. * * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD) * @since 100.0.2 */ class Context extends \Magento\Backend\Block\Template\Context @@ -70,6 +72,7 @@ class Context extends \Magento\Backend\Block\Template\Context * @param \Magento\Framework\Code\NameBuilder $nameBuilder * @param \Magento\Backend\Block\Widget\Button\ButtonList $buttonList * @param Button\ToolbarInterface $toolbar + * @param LockGuardedCacheLoader|null $lockQuery * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -105,7 +108,8 @@ public function __construct( \Magento\Framework\Data\Form\FormKey $formKey, \Magento\Framework\Code\NameBuilder $nameBuilder, Button\ButtonList $buttonList, - Button\ToolbarInterface $toolbar + Button\ToolbarInterface $toolbar, + LockGuardedCacheLoader $lockQuery = null ) { parent::__construct( $request, @@ -137,7 +141,8 @@ public function __construct( $backendSession, $mathRandom, $formKey, - $nameBuilder + $nameBuilder, + $lockQuery ); $this->buttonList = $buttonList; 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..335ff3bf85e7a 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Delete.php @@ -1,14 +1,20 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Backend\Controller\Adminhtml\System\Design; -class Delete extends \Magento\Backend\Controller\Adminhtml\System\Design +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Delete store design schedule action. + */ +class Delete extends \Magento\Backend\Controller\Adminhtml\System\Design implements HttpPostActionInterface { /** + * Execute action. + * * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() diff --git a/app/code/Magento/Backend/Model/Auth/Session.php b/app/code/Magento/Backend/Model/Auth/Session.php index 61db71c1803e2..809b78b7b98bc 100644 --- a/app/code/Magento/Backend/Model/Auth/Session.php +++ b/app/code/Magento/Backend/Model/Auth/Session.php @@ -5,20 +5,17 @@ */ namespace Magento\Backend\Model\Auth; -use Magento\Framework\Acl; -use Magento\Framework\AclFactory; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; use Magento\Framework\Stdlib\CookieManagerInterface; -use Magento\Backend\Spi\SessionUserHydratorInterface; -use Magento\Backend\Spi\SessionAclHydratorInterface; -use Magento\User\Model\User; -use Magento\User\Model\UserFactory; /** * Backend Auth session model * * @api + * @method \Magento\User\Model\User|null getUser() + * @method \Magento\Backend\Model\Auth\Session setUser(\Magento\User\Model\User $value) + * @method \Magento\Framework\Acl|null getAcl() + * @method \Magento\Backend\Model\Auth\Session setAcl(\Magento\Framework\Acl $value) * @method int getUpdatedAt() * @method \Magento\Backend\Model\Auth\Session setUpdatedAt(int $value) * @@ -59,36 +56,6 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage */ protected $_config; - /** - * @var SessionUserHydratorInterface - */ - private $userHydrator; - - /** - * @var SessionAclHydratorInterface - */ - private $aclHydrator; - - /** - * @var UserFactory - */ - private $userFactory; - - /** - * @var AclFactory - */ - private $aclFactory; - - /** - * @var User|null - */ - private $user; - - /** - * @var Acl|null - */ - private $acl; - /** * @param \Magento\Framework\App\Request\Http $request * @param \Magento\Framework\Session\SidResolverInterface $sidResolver @@ -103,10 +70,6 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage * @param \Magento\Backend\Model\UrlInterface $backendUrl * @param \Magento\Backend\App\ConfigInterface $config * @throws \Magento\Framework\Exception\SessionException - * @param SessionUserHydratorInterface|null $userHydrator - * @param SessionAclHydratorInterface|null $aclHydrator - * @param UserFactory|null $userFactory - * @param AclFactory|null $aclFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -121,19 +84,11 @@ public function __construct( \Magento\Framework\App\State $appState, \Magento\Framework\Acl\Builder $aclBuilder, \Magento\Backend\Model\UrlInterface $backendUrl, - \Magento\Backend\App\ConfigInterface $config, - ?SessionUserHydratorInterface $userHydrator = null, - ?SessionAclHydratorInterface $aclHydrator = null, - ?UserFactory $userFactory = null, - ?AclFactory $aclFactory = null + \Magento\Backend\App\ConfigInterface $config ) { $this->_config = $config; $this->_aclBuilder = $aclBuilder; $this->_backendUrl = $backendUrl; - $this->userHydrator = $userHydrator ?? ObjectManager::getInstance()->get(SessionUserHydratorInterface::class); - $this->aclHydrator = $aclHydrator ?? ObjectManager::getInstance()->get(SessionAclHydratorInterface::class); - $this->userFactory = $userFactory ?? ObjectManager::getInstance()->get(UserFactory::class); - $this->aclFactory = $aclFactory ?? ObjectManager::getInstance()->get(AclFactory::class); parent::__construct( $request, $sidResolver, @@ -192,6 +147,7 @@ public function isAllowed($resource, $privilege = null) return $acl->isAllowed($user->getAclRole(), null, $privilege); } } catch (\Exception $e) { + return false; } } } @@ -276,16 +232,6 @@ public function processLogin() return $this; } - /** - * @inheritDoc - */ - public function destroy(array $options = null) - { - $this->user = null; - $this->acl = null; - parent::destroy($options); - } - /** * Process of configuring of current auth storage when logout was performed * @@ -309,136 +255,4 @@ public function isValidForPath($path) { return true; } - - /** - * Logged-in user. - * - * @return User|null - */ - public function getUser() - { - if (!$this->user) { - $userData = $this->getUserData(); - if ($userData) { - /** @var User $user */ - $user = $this->userFactory->create(); - $this->userHydrator->hydrate($user, $userData); - $this->user = $user; - } - } - - return $this->user; - } - - /** - * Set logged-in user instance. - * - * @param User|null $user - * @return Session - */ - public function setUser($user) - { - $this->setUserData(null); - if ($user) { - $this->setUserData($this->userHydrator->extract($user)); - } - $this->user = $user; - - return $this; - } - - /** - * Is user logged in? - * - * @return bool - */ - public function hasUser() - { - return $this->user || $this->hasUserData(); - } - - /** - * Remove logged-in user. - * - * @return Session - */ - public function unsUser() - { - $this->user = null; - return $this->unsUserData(); - } - - /** - * Logged-in user's ACL data. - * - * @return Acl|null - */ - public function getAcl() - { - if (!$this->acl) { - $aclData = $this->getUserAclData(); - if ($aclData) { - /** @var Acl $acl */ - $acl = $this->aclFactory->create(); - $this->aclHydrator->hydrate($acl, $aclData); - $this->acl = $acl; - } - } - - return $this->acl; - } - - /** - * Set logged-in user's ACL data instance. - * - * @param Acl|null $acl - * @return Session - */ - public function setAcl($acl) - { - $this->setUserAclData(null); - if ($acl) { - $this->setUserAclData($this->aclHydrator->extract($acl)); - } - $this->acl = $acl; - - return $this; - } - - /** - * Whether ACL data is present. - * - * @return bool - */ - public function hasAcl() - { - return $this->acl || $this->hasUserAclData(); - } - - /** - * Remove ACL data. - * - * @return Session - */ - public function unsAcl() - { - $this->acl = null; - return $this->unsUserAclData(); - } - - /** - * @inheritDoc - */ - public function writeClose() - { - //Updating data in session in case these objects has been changed. - if ($this->user) { - $this->setUser($this->user); - } - if ($this->acl) { - $this->setAcl($this->acl); - } - - parent::writeClose(); - } } diff --git a/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php deleted file mode 100644 index 34e01be696672..0000000000000 --- a/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\Backend\Model\Auth; - -use Magento\Backend\Spi\SessionAclHydratorInterface; -use Magento\Framework\Acl; - -/** - * @inheritDoc - */ -class SessionAclHydrator extends Acl implements SessionAclHydratorInterface -{ - /** - * @inheritDoc - */ - public function extract(Acl $acl): array - { - return ['rules' => $acl->_rules, 'resources' => $acl->_resources, 'roles' => $acl->_roleRegistry]; - } - - /** - * @inheritDoc - */ - public function hydrate(Acl $target, array $data): void - { - $target->_rules = $data['rules']; - $target->_resources = $data['resources']; - $target->_roleRegistry = $data['roles']; - } -} diff --git a/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php deleted file mode 100644 index 6dee8b7b302c8..0000000000000 --- a/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\Backend\Model\Auth; - -use Magento\Backend\Spi\SessionUserHydratorInterface; -use Magento\User\Model\User; -use Magento\Authorization\Model\Role; -use Magento\Authorization\Model\RoleFactory; - -/** - * @inheritDoc - */ -class SessionUserHydrator implements SessionUserHydratorInterface -{ - /** - * @var RoleFactory - */ - private $roleFactory; - - /** - * @param RoleFactory $roleFactory - */ - public function __construct(RoleFactory $roleFactory) - { - $this->roleFactory = $roleFactory; - } - - /** - * @inheritDoc - */ - public function extract(User $user): array - { - return ['data' => $user->getData(), 'role_data' => $user->getRole()->getData()]; - } - - /** - * @inheritDoc - */ - public function hydrate(User $target, array $data): void - { - $target->setData($data['data']); - /** @var Role $role */ - $role = $this->roleFactory->create(); - $role->setData($data['role_data']); - $target->setData('extracted_role', $role); - $target->getRole(); - } -} diff --git a/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php b/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php deleted file mode 100644 index 7227cc92fcc8e..0000000000000 --- a/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.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\Backend\Spi; - -use Magento\Framework\Acl; - -/** - * Extract/hydrate user's ACL data to/from session. - */ -interface SessionAclHydratorInterface -{ - /** - * Extract ACL data to store in session. - * - * @param Acl $acl - * @return array Array of scalars. - */ - public function extract(Acl $acl): array; - - /** - * Fill ACL object with data from session. - * - * @param Acl $target - * @param array $data Data from session. - * @return void - */ - public function hydrate(Acl $target, array $data): void; -} diff --git a/app/code/Magento/Backend/Spi/SessionUserHydratorInterface.php b/app/code/Magento/Backend/Spi/SessionUserHydratorInterface.php deleted file mode 100644 index 211c7b01df3be..0000000000000 --- a/app/code/Magento/Backend/Spi/SessionUserHydratorInterface.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\Backend\Spi; - -use Magento\User\Model\User; - -/** - * Extract/hydrate user data to/from session. - */ -interface SessionUserHydratorInterface -{ - /** - * Extract user data to store in session. - * - * @param User $user - * @return array Array of scalars. - */ - public function extract(User $user): array; - - /** - * Fill User object with data from session. - * - * @param User $target - * @param array $data Data from session. - * @return void - */ - public function hydrate(User $target, array $data): void; -} diff --git a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php new file mode 100644 index 0000000000000..f1a4bc355b08e --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php @@ -0,0 +1,273 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Backend\Test\Unit\Model\Auth; + +use Magento\Backend\Model\Auth\Session; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Class SessionTest tests Magento\Backend\Model\Auth\Session + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SessionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Backend\App\Config | \PHPUnit_Framework_MockObject_MockObject + */ + protected $config; + + /** + * @var \Magento\Framework\Session\Config | \PHPUnit_Framework_MockObject_MockObject + */ + protected $sessionConfig; + + /** + * @var \Magento\Framework\Stdlib\CookieManagerInterface | \PHPUnit_Framework_MockObject_MockObject + */ + protected $cookieManager; + + /** + * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory | \PHPUnit_Framework_MockObject_MockObject + */ + protected $cookieMetadataFactory; + + /** + * @var \Magento\Framework\Session\Storage | \PHPUnit_Framework_MockObject_MockObject + */ + protected $storage; + + /** + * @var \Magento\Framework\Acl\Builder | \PHPUnit_Framework_MockObject_MockObject + */ + protected $aclBuilder; + + /** + * @var Session + */ + protected $session; + + protected function setUp() + { + $this->cookieMetadataFactory = $this->createPartialMock( + \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class, + ['createPublicCookieMetadata'] + ); + + $this->config = $this->createPartialMock(\Magento\Backend\App\Config::class, ['getValue']); + $this->cookieManager = $this->createPartialMock( + \Magento\Framework\Stdlib\Cookie\PhpCookieManager::class, + ['getCookie', 'setPublicCookie'] + ); + $this->storage = $this->createPartialMock( + \Magento\Framework\Session\Storage::class, + ['getUser', 'getAcl', 'setAcl'] + ); + $this->sessionConfig = $this->createPartialMock( + \Magento\Framework\Session\Config::class, + ['getCookiePath', 'getCookieDomain', 'getCookieSecure', 'getCookieHttpOnly'] + ); + $this->aclBuilder = $this->getMockBuilder(\Magento\Framework\Acl\Builder::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManager = new ObjectManager($this); + $this->session = $objectManager->getObject( + \Magento\Backend\Model\Auth\Session::class, + [ + 'config' => $this->config, + 'sessionConfig' => $this->sessionConfig, + 'cookieManager' => $this->cookieManager, + 'cookieMetadataFactory' => $this->cookieMetadataFactory, + 'storage' => $this->storage, + 'aclBuilder' => $this->aclBuilder + ] + ); + } + + protected function tearDown() + { + $this->config = null; + $this->sessionConfig = null; + $this->session = null; + } + + /** + * @dataProvider refreshAclDataProvider + * @param $isUserPassedViaParams + */ + public function testRefreshAcl($isUserPassedViaParams) + { + $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock(); + $this->aclBuilder->expects($this->any())->method('getAcl')->willReturn($aclMock); + $userMock = $this->getMockBuilder(\Magento\User\Model\User::class) + ->setMethods(['getReloadAclFlag', 'setReloadAclFlag', 'unsetData', 'save']) + ->disableOriginalConstructor() + ->getMock(); + $userMock->expects($this->any())->method('getReloadAclFlag')->willReturn(true); + $userMock->expects($this->once())->method('setReloadAclFlag')->with('0')->willReturnSelf(); + $userMock->expects($this->once())->method('save'); + $this->storage->expects($this->once())->method('setAcl')->with($aclMock); + $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock); + if ($isUserPassedViaParams) { + $this->session->refreshAcl($userMock); + } else { + $this->storage->expects($this->once())->method('getUser')->willReturn($userMock); + $this->session->refreshAcl(); + } + $this->assertSame($aclMock, $this->session->getAcl()); + } + + /** + * @return array + */ + public function refreshAclDataProvider() + { + return [ + 'User set via params' => [true], + 'User set to session object' => [false] + ]; + } + + public function testIsLoggedInPositive() + { + $user = $this->createPartialMock(\Magento\User\Model\User::class, ['getId', '__wakeup']); + $user->expects($this->once()) + ->method('getId') + ->will($this->returnValue(1)); + + $this->storage->expects($this->any()) + ->method('getUser') + ->will($this->returnValue($user)); + + $this->assertTrue($this->session->isLoggedIn()); + } + + public function testProlong() + { + $name = session_name(); + $cookie = 'cookie'; + $lifetime = 900; + $path = '/'; + $domain = 'magento2'; + $secure = true; + $httpOnly = true; + + $this->config->expects($this->once()) + ->method('getValue') + ->with(\Magento\Backend\Model\Auth\Session::XML_PATH_SESSION_LIFETIME) + ->willReturn($lifetime); + $cookieMetadata = $this->createMock(\Magento\Framework\Stdlib\Cookie\PublicCookieMetadata::class); + $cookieMetadata->expects($this->once()) + ->method('setDuration') + ->with($lifetime) + ->will($this->returnSelf()); + $cookieMetadata->expects($this->once()) + ->method('setPath') + ->with($path) + ->will($this->returnSelf()); + $cookieMetadata->expects($this->once()) + ->method('setDomain') + ->with($domain) + ->will($this->returnSelf()); + $cookieMetadata->expects($this->once()) + ->method('setSecure') + ->with($secure) + ->will($this->returnSelf()); + $cookieMetadata->expects($this->once()) + ->method('setHttpOnly') + ->with($httpOnly) + ->will($this->returnSelf()); + + $this->cookieMetadataFactory->expects($this->once()) + ->method('createPublicCookieMetadata') + ->will($this->returnValue($cookieMetadata)); + + $this->cookieManager->expects($this->once()) + ->method('getCookie') + ->with($name) + ->will($this->returnValue($cookie)); + $this->cookieManager->expects($this->once()) + ->method('setPublicCookie') + ->with($name, $cookie, $cookieMetadata); + + $this->sessionConfig->expects($this->once()) + ->method('getCookiePath') + ->will($this->returnValue($path)); + $this->sessionConfig->expects($this->once()) + ->method('getCookieDomain') + ->will($this->returnValue($domain)); + $this->sessionConfig->expects($this->once()) + ->method('getCookieSecure') + ->will($this->returnValue($secure)); + $this->sessionConfig->expects($this->once()) + ->method('getCookieHttpOnly') + ->will($this->returnValue($httpOnly)); + + $this->session->prolong(); + + $this->assertLessThanOrEqual(time(), $this->session->getUpdatedAt()); + } + + /** + * @dataProvider isAllowedDataProvider + * @param bool $isUserDefined + * @param bool $isAclDefined + * @param bool $isAllowed + * @param true $expectedResult + */ + public function testIsAllowed($isUserDefined, $isAclDefined, $isAllowed, $expectedResult) + { + $userAclRole = 'userAclRole'; + if ($isAclDefined) { + $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock(); + $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock); + } + if ($isUserDefined) { + $userMock = $this->getMockBuilder(\Magento\User\Model\User::class)->disableOriginalConstructor()->getMock(); + $this->storage->expects($this->once())->method('getUser')->willReturn($userMock); + } + if ($isAclDefined && $isUserDefined) { + $userMock->expects($this->any())->method('getAclRole')->willReturn($userAclRole); + $aclMock->expects($this->once())->method('isAllowed')->with($userAclRole)->willReturn($isAllowed); + } + + $this->assertEquals($expectedResult, $this->session->isAllowed('resource')); + } + + /** + * @return array + */ + public function isAllowedDataProvider() + { + return [ + "Negative: User not defined" => [false, true, true, false], + "Negative: Acl not defined" => [true, false, true, false], + "Negative: Permission denied" => [true, true, false, false], + "Positive: Permission granted" => [true, true, false, false], + ]; + } + + /** + * @dataProvider firstPageAfterLoginDataProvider + * @param bool $isFirstPageAfterLogin + */ + public function testFirstPageAfterLogin($isFirstPageAfterLogin) + { + $this->session->setIsFirstPageAfterLogin($isFirstPageAfterLogin); + $this->assertEquals($isFirstPageAfterLogin, $this->session->isFirstPageAfterLogin()); + } + + /** + * @return array + */ + public function firstPageAfterLoginDataProvider() + { + return [ + 'First page after login' => [true], + 'Not first page after login' => [false], + ]; + } +} diff --git a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php new file mode 100644 index 0000000000000..77c428a6a116a --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Backend\Test\Unit\Model\Authorization; + +/** + * Class RoleLocatorTest + */ +class RoleLocatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Backend\Model\Authorization\RoleLocator + */ + protected $_model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $_sessionMock = []; + + protected function setUp() + { + $this->_sessionMock = $this->createPartialMock( + \Magento\Backend\Model\Auth\Session::class, + ['getUser', 'getAclRole', 'hasUser'] + ); + $this->_model = new \Magento\Backend\Model\Authorization\RoleLocator($this->_sessionMock); + } + + public function testGetAclRoleIdReturnsCurrentUserAclRoleId() + { + $this->_sessionMock->expects($this->once())->method('hasUser')->will($this->returnValue(true)); + $this->_sessionMock->expects($this->once())->method('getUser')->will($this->returnSelf()); + $this->_sessionMock->expects($this->once())->method('getAclRole')->will($this->returnValue('some_role')); + $this->assertEquals('some_role', $this->_model->getAclRoleId()); + } +} diff --git a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php b/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php new file mode 100644 index 0000000000000..ce2b65a2249ac --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Backend\Test\Unit\Model\Locale; + +use Magento\Framework\Locale\Resolver; + +/** + * Class ManagerTest + */ +class ManagerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Backend\Model\Locale\Manager + */ + protected $_model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\TranslateInterface + */ + protected $_translator; + + /** + * @var \Magento\Backend\Model\Session + */ + protected $_session; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\Model\Auth\Session + */ + protected $_authSession; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\App\ConfigInterface + */ + protected $_backendConfig; + + protected function setUp() + { + $this->_session = $this->createMock(\Magento\Backend\Model\Session::class); + + $this->_authSession = $this->createPartialMock(\Magento\Backend\Model\Auth\Session::class, ['getUser']); + + $this->_backendConfig = $this->getMockForAbstractClass( + \Magento\Backend\App\ConfigInterface::class, + [], + '', + false + ); + + $userMock = new \Magento\Framework\DataObject(); + + $this->_authSession->expects($this->any())->method('getUser')->will($this->returnValue($userMock)); + + $this->_translator = $this->getMockBuilder(\Magento\Framework\TranslateInterface::class) + ->setMethods(['init', 'setLocale']) + ->getMockForAbstractClass(); + + $this->_translator->expects($this->any())->method('setLocale')->will($this->returnValue($this->_translator)); + + $this->_translator->expects($this->any())->method('init')->will($this->returnValue(false)); + + $this->_model = new \Magento\Backend\Model\Locale\Manager( + $this->_session, + $this->_authSession, + $this->_translator, + $this->_backendConfig + ); + } + + /** + * @return array + */ + public function switchBackendInterfaceLocaleDataProvider() + { + return ['case1' => ['locale' => 'de_DE'], 'case2' => ['locale' => 'en_US']]; + } + + /** + * @param string $locale + * @dataProvider switchBackendInterfaceLocaleDataProvider + * @covers \Magento\Backend\Model\Locale\Manager::switchBackendInterfaceLocale + */ + public function testSwitchBackendInterfaceLocale($locale) + { + $this->_model->switchBackendInterfaceLocale($locale); + + $userInterfaceLocale = $this->_authSession->getUser()->getInterfaceLocale(); + $this->assertEquals($userInterfaceLocale, $locale); + + $sessionLocale = $this->_session->getSessionLocale(); + $this->assertEquals($sessionLocale, null); + } + + /** + * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale + */ + public function testGetUserInterfaceLocaleDefault() + { + $locale = $this->_model->getUserInterfaceLocale(); + + $this->assertEquals($locale, Resolver::DEFAULT_LOCALE); + } + + /** + * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale + */ + public function testGetUserInterfaceLocale() + { + $this->_model->switchBackendInterfaceLocale('de_DE'); + $locale = $this->_model->getUserInterfaceLocale(); + + $this->assertEquals($locale, 'de_DE'); + } + + /** + * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale + */ + public function testGetUserInterfaceGeneralLocale() + { + $this->_backendConfig->expects($this->any()) + ->method('getValue') + ->with('general/locale/code') + ->willReturn('test_locale'); + $locale = $this->_model->getUserInterfaceLocale(); + $this->assertEquals($locale, 'test_locale'); + } +} diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index e54bd136b3494..f9408768136bb 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -22,7 +22,6 @@ "magento/module-store": "*", "magento/module-translation": "*", "magento/module-ui": "*", - "magento/module-authorization": "*", "magento/module-user": "*" }, "suggest": { diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 65744e56d94ac..c762dbf58de62 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -182,10 +182,6 @@ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>Minification is not applied in developer mode.</comment> </field> - <field id="move_inline_to_bottom" translate="label" type="select" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> - <label>Move JS code to the bottom of the page</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> </group> <group id="css" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="1"> <label>CSS Settings</label> diff --git a/app/code/Magento/Backend/etc/di.xml b/app/code/Magento/Backend/etc/di.xml index 41db85b9323a8..c526703da9975 100644 --- a/app/code/Magento/Backend/etc/di.xml +++ b/app/code/Magento/Backend/etc/di.xml @@ -198,8 +198,4 @@ <argument name="anchorRenderer" xsi:type="object">Magento\Backend\Block\AnchorRenderer</argument> </arguments> </type> - <preference for="Magento\Backend\Spi\SessionUserHydratorInterface" - type="Magento\Backend\Model\Auth\SessionUserHydrator" /> - <preference for="Magento\Backend\Spi\SessionAclHydratorInterface" - type="Magento\Backend\Model\Auth\SessionAclHydrator" /> </config> diff --git a/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml b/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml index e0a9e46bd7c5c..36eddcf5819d9 100644 --- a/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml +++ b/app/code/Magento/Braintree/view/frontend/templates/paypal/button.phtml @@ -8,7 +8,7 @@ * @var \Magento\Braintree\Block\Paypal\Button $block */ -$id = $block->getContainerId() . mt_rand(); +$id = $block->getContainerId() . random_int(0, PHP_INT_MAX); $config = [ 'Magento_Braintree/js/paypal/button' => [ diff --git a/app/code/Magento/Captcha/CustomerData/Captcha.php b/app/code/Magento/Captcha/CustomerData/Captcha.php index a744daacdc673..e07bf953abaa3 100644 --- a/app/code/Magento/Captcha/CustomerData/Captcha.php +++ b/app/code/Magento/Captcha/CustomerData/Captcha.php @@ -9,11 +9,18 @@ namespace Magento\Captcha\CustomerData; use Magento\Customer\CustomerData\SectionSourceInterface; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Captcha\Model\DefaultModel; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; /** - * Captcha section + * Captcha section. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ -class Captcha extends \Magento\Framework\DataObject implements SectionSourceInterface +class Captcha extends DataObject implements SectionSourceInterface { /** * @var array @@ -21,24 +28,31 @@ class Captcha extends \Magento\Framework\DataObject implements SectionSourceInte private $formIds; /** - * @var \Magento\Captcha\Helper\Data + * @var CaptchaHelper */ private $helper; /** - * @param \Magento\Captcha\Helper\Data $helper + * @var CustomerSession + */ + private $customerSession; + + /** + * @param CaptchaHelper $helper * @param array $formIds * @param array $data - * @codeCoverageIgnore + * @param CustomerSession|null $customerSession */ public function __construct( - \Magento\Captcha\Helper\Data $helper, + CaptchaHelper $helper, array $formIds, - array $data = [] + array $data = [], + ?CustomerSession $customerSession = null ) { - parent::__construct($data); $this->helper = $helper; $this->formIds = $formIds; + parent::__construct($data); + $this->customerSession = $customerSession ?? ObjectManager::getInstance()->get(CustomerSession::class); } /** @@ -49,9 +63,15 @@ public function getSectionData() :array $data = []; foreach ($this->formIds as $formId) { + /** @var DefaultModel $captchaModel */ $captchaModel = $this->helper->getCaptcha($formId); + $login = ''; + if ($this->customerSession->isLoggedIn()) { + $login = $this->customerSession->getCustomerData()->getEmail(); + } + $required = $captchaModel->isRequired($login); $data[$formId] = [ - 'isRequired' => $captchaModel->isRequired(), + 'isRequired' => $required, 'timestamp' => time() ]; } diff --git a/app/code/Magento/Captcha/Model/DefaultModel.php b/app/code/Magento/Captcha/Model/DefaultModel.php index 483f9c3fb4d20..bbbbfb0a36e08 100644 --- a/app/code/Magento/Captcha/Model/DefaultModel.php +++ b/app/code/Magento/Captcha/Model/DefaultModel.php @@ -3,13 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Captcha\Model; use Magento\Captcha\Helper\Data; +use Magento\Framework\Math\Random; /** * Implementation of \Zend\Captcha\Image * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * * @api * @since 100.0.2 */ @@ -83,24 +88,32 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model */ private $words; + /** + * @var Random + */ + private $randomMath; + /** * @param \Magento\Framework\Session\SessionManagerInterface $session * @param \Magento\Captcha\Helper\Data $captchaData * @param ResourceModel\LogFactory $resLogFactory * @param string $formId + * @param Random $randomMath * @throws \Zend\Captcha\Exception\ExtensionNotLoadedException */ public function __construct( \Magento\Framework\Session\SessionManagerInterface $session, \Magento\Captcha\Helper\Data $captchaData, \Magento\Captcha\Model\ResourceModel\LogFactory $resLogFactory, - $formId + $formId, + Random $randomMath = null ) { parent::__construct(); $this->session = $session; $this->captchaData = $captchaData; $this->resLogFactory = $resLogFactory; $this->formId = $formId; + $this->randomMath = $randomMath ?? \Magento\Framework\App\ObjectManager::getInstance()->get(Random::class); } /** @@ -382,23 +395,9 @@ public function setShowCaptchaInSession($value = true) */ protected function generateWord() { - $word = ''; - $symbols = $this->getSymbols(); + $symbols = (string)$this->captchaData->getConfig('symbols'); $wordLen = $this->getWordLen(); - for ($i = 0; $i < $wordLen; $i++) { - $word .= $symbols[array_rand($symbols)]; - } - return $word; - } - - /** - * Get symbols array to use for word generation - * - * @return array - */ - private function getSymbols() - { - return str_split((string)$this->captchaData->getConfig('symbols')); + return $this->randomMath->getRandomString($wordLen, $symbols); } /** @@ -508,7 +507,7 @@ private function getWords() /** * Set captcha word * - * @param string $word + * @param string $word * @return $this * @since 100.2.0 */ @@ -562,7 +561,7 @@ protected function randomSize() */ protected function gc() { - //do nothing + return; // required for static testing to pass } /** diff --git a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php index 39579616fa928..d83abc7a6c7d1 100644 --- a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php +++ b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php @@ -5,19 +5,31 @@ */ namespace Magento\Captcha\Observer; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Request\Http as HttpRequest; + +/** + * Extract given captcha word. + */ class CaptchaStringResolver { /** * Get Captcha String * - * @param \Magento\Framework\App\RequestInterface $request + * @param \Magento\Framework\App\RequestInterface|HttpRequest $request * @param string $formId * @return string */ - public function resolve(\Magento\Framework\App\RequestInterface $request, $formId) + public function resolve(RequestInterface $request, $formId) { $captchaParams = $request->getPost(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE); + if (!empty($captchaParams) && !empty($captchaParams[$formId])) { + $value = $captchaParams[$formId]; + } else { + //For Web APIs + $value = $request->getHeader('X-Captcha'); + } - return $captchaParams[$formId] ?? ''; + return $value; } } diff --git a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php index eef75d2c01ec7..b569803078457 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Captcha\Test\Unit\Model; +use Magento\Framework\Math\Random; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -253,13 +257,15 @@ protected function _getSessionStub() ->getMock(); $session->expects($this->any())->method('isLoggedIn')->will($this->returnValue(false)); - $session->setData([ - 'user_create_word' => [ - 'data' => 'AbCdEf5', - 'words' => 'AbCdEf5', - 'expires' => time() + self::EXPIRE_FRAME + $session->setData( + [ + 'user_create_word' => [ + 'data' => 'AbCdEf5', + 'words' => 'AbCdEf5', + 'expires' => time() + self::EXPIRE_FRAME + ] ] - ]); + ); return $session; } @@ -375,4 +381,38 @@ public function isShownToLoggedInUserDataProvider() [false, 'user_forgotpassword'] ]; } + + /** + * @param string $string + * @dataProvider generateWordProvider + * @throws \ReflectionException + */ + public function testGenerateWord($string) + { + $randomMock = $this->createMock(Random::class); + $randomMock->expects($this->once()) + ->method('getRandomString') + ->will($this->returnValue($string)); + $captcha = new \Magento\Captcha\Model\DefaultModel( + $this->session, + $this->_getHelperStub(), + $this->_resLogFactory, + 'user_create', + $randomMock + ); + $method = new \ReflectionMethod($captcha, 'generateWord'); + $method->setAccessible(true); + $this->assertEquals($string, $method->invoke($captcha)); + } + /** + * @return array + */ + public function generateWordProvider() + { + return [ + ['ABC123'], + ['1234567890'], + ['The quick brown fox jumps over the lazy dog.'] + ]; + } } diff --git a/app/code/Magento/Catalog/Block/Product/Context.php b/app/code/Magento/Catalog/Block/Product/Context.php index 4ca9e6b290bb5..db18eb2bc8a7d 100644 --- a/app/code/Magento/Catalog/Block/Product/Context.php +++ b/app/code/Magento/Catalog/Block/Product/Context.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Block\Product; +use Magento\Framework\Cache\LockGuardedCacheLoader; + /** * Constructor modification point for Magento\Catalog\Block\Product\AbstractProduct. * @@ -17,7 +19,7 @@ * the classes they were introduced for. * * @deprecated 101.1.0 - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD) */ class Context extends \Magento\Framework\View\Element\Template\Context { @@ -124,6 +126,7 @@ class Context extends \Magento\Framework\View\Element\Template\Context * @param ImageBuilder $imageBuilder * @param ReviewRendererInterface $reviewRenderer * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param LockGuardedCacheLoader|null $lockQuery * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -164,7 +167,8 @@ public function __construct( \Magento\Catalog\Helper\Image $imageHelper, \Magento\Catalog\Block\Product\ImageBuilder $imageBuilder, ReviewRendererInterface $reviewRenderer, - \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + LockGuardedCacheLoader $lockQuery = null ) { $this->imageHelper = $imageHelper; $this->imageBuilder = $imageBuilder; @@ -203,11 +207,14 @@ public function __construct( $storeManager, $pageConfig, $resolver, - $validator + $validator, + $lockQuery ); } /** + * Get Stock registry. + * * @return \Magento\CatalogInventory\Api\StockRegistryInterface */ public function getStockRegistry() @@ -216,6 +223,8 @@ public function getStockRegistry() } /** + * Get cart helper. + * * @return \Magento\Checkout\Helper\Cart */ public function getCartHelper() @@ -224,6 +233,8 @@ public function getCartHelper() } /** + * Get catalog config. + * * @return \Magento\Catalog\Model\Config */ public function getCatalogConfig() @@ -232,6 +243,8 @@ public function getCatalogConfig() } /** + * Get catalog helper. + * * @return \Magento\Catalog\Helper\Data */ public function getCatalogHelper() @@ -240,6 +253,8 @@ public function getCatalogHelper() } /** + * Get compare product. + * * @return \Magento\Catalog\Helper\Product\Compare */ public function getCompareProduct() @@ -248,6 +263,8 @@ public function getCompareProduct() } /** + * Get image helper. + * * @return \Magento\Catalog\Helper\Image */ public function getImageHelper() @@ -256,6 +273,8 @@ public function getImageHelper() } /** + * Get image builder. + * * @return \Magento\Catalog\Block\Product\ImageBuilder */ public function getImageBuilder() @@ -264,6 +283,8 @@ public function getImageBuilder() } /** + * Get math random. + * * @return \Magento\Framework\Math\Random */ public function getMathRandom() @@ -272,6 +293,8 @@ public function getMathRandom() } /** + * Get registry. + * * @return \Magento\Framework\Registry */ public function getRegistry() @@ -280,6 +303,8 @@ public function getRegistry() } /** + * Get tax data. + * * @return \Magento\Tax\Helper\Data */ public function getTaxData() @@ -288,6 +313,8 @@ public function getTaxData() } /** + * Get wishlist helper. + * * @return \Magento\Wishlist\Helper\Data */ public function getWishlistHelper() @@ -296,6 +323,8 @@ public function getWishlistHelper() } /** + * Get review renderer. + * * @return \Magento\Catalog\Block\Product\ReviewRendererInterface */ public function getReviewRenderer() 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 d9d663b32f4de..81d7e18d45519 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 @@ -58,7 +58,7 @@ public function __construct( * * @return string */ - public function getValuesHtml() + public function getValuesHtml(): string { $option = $this->getOption(); $optionType = $option->getType(); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Widget/Chooser.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Widget/Chooser.php index 933b5eaafbb39..113b048f7c98b 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Widget/Chooser.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Widget/Chooser.php @@ -1,12 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Controller\Adminhtml\Product\Widget; -class Chooser extends \Magento\Backend\App\Action +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; + +/** + * Controller to build Chooser container. + */ +class Chooser extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -23,23 +28,31 @@ class Chooser extends \Magento\Backend\App\Action */ protected $layoutFactory; + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param \Magento\Framework\Escaper|null $escaper */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, - \Magento\Framework\View\LayoutFactory $layoutFactory + \Magento\Framework\View\LayoutFactory $layoutFactory, + \Magento\Framework\Escaper $escaper = null ) { parent::__construct($context); $this->resultRawFactory = $resultRawFactory; $this->layoutFactory = $layoutFactory; + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(\Magento\Framework\Escaper::class); } /** - * Chooser Source action + * Chooser Source action. * * @return \Magento\Framework\Controller\Result\Raw */ @@ -55,11 +68,11 @@ public function execute() '', [ 'data' => [ - 'id' => $uniqId, + 'id' => $this->escaper->escapeHtml($uniqId), 'use_massaction' => $massAction, 'product_type_id' => $productTypeId, - 'category_id' => $this->getRequest()->getParam('category_id'), - ] + 'category_id' => (int)$this->getRequest()->getParam('category_id'), + ], ] ); @@ -71,10 +84,10 @@ public function execute() '', [ 'data' => [ - 'id' => $uniqId . 'Tree', + 'id' => $this->escaper->escapeHtml($uniqId) . 'Tree', 'node_click_listener' => $productsGrid->getCategoryClickListenerJs(), 'with_empty_node' => true, - ] + ], ] ); @@ -86,6 +99,7 @@ public function execute() /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ $resultRaw = $this->resultRawFactory->create(); + return $resultRaw->setContents($html); } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php index d99901c915a10..f5c3171a3fe90 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php @@ -9,10 +9,13 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\NoSuchEntityException; +/** + * Add item to compare list action. + */ class Add extends \Magento\Catalog\Controller\Product\Compare implements HttpPostActionInterface { /** - * Add item to compare list + * Add item to compare list. * * @return \Magento\Framework\Controller\ResultInterface */ @@ -27,12 +30,13 @@ public function execute() if ($productId && ($this->_customerVisitor->getId() || $this->_customerSession->isLoggedIn())) { $storeId = $this->_storeManager->getStore()->getId(); try { + /** @var \Magento\Catalog\Model\Product $product */ $product = $this->productRepository->getById($productId, false, $storeId); } catch (NoSuchEntityException $e) { $product = null; } - if ($product) { + if ($product && $product->isSalable()) { $this->_catalogProductCompareList->addProduct($product); $productName = $this->_objectManager->get( \Magento\Framework\Escaper::class @@ -41,7 +45,7 @@ public function execute() 'addCompareSuccessMessage', [ 'product_name' => $productName, - 'compare_list_url' => $this->_url->getUrl('catalog/product_compare') + 'compare_list_url' => $this->_url->getUrl('catalog/product_compare'), ] ); @@ -50,6 +54,7 @@ public function execute() $this->_objectManager->get(\Magento\Catalog\Helper\Product\Compare::class)->calculate(); } + return $resultRedirect->setRefererOrBaseUrl(); } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php index eac0ddf94af20..acf0f1b754c12 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php @@ -9,10 +9,13 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\NoSuchEntityException; +/** + * Remove item from compare list action. + */ class Remove extends \Magento\Catalog\Controller\Product\Compare implements HttpPostActionInterface { /** - * Remove item from compare list + * Remove item from compare list. * * @return \Magento\Framework\Controller\ResultInterface */ @@ -22,12 +25,13 @@ public function execute() if ($productId) { $storeId = $this->_storeManager->getStore()->getId(); try { + /** @var \Magento\Catalog\Model\Product $product */ $product = $this->productRepository->getById($productId, false, $storeId); } catch (NoSuchEntityException $e) { $product = null; } - if ($product) { + if ($product && $product->isSalable()) { /** @var $item \Magento\Catalog\Model\Product\Compare\Item */ $item = $this->_compareItemFactory->create(); if ($this->_customerSession->isLoggedIn()) { @@ -59,6 +63,7 @@ public function execute() if (!$this->getRequest()->getParam('isAjax', false)) { $resultRedirect = $this->resultRedirectFactory->create(); + return $resultRedirect->setRefererOrBaseUrl(); } } diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index d911bec0aaac9..9d6d7e41ff34e 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -5,10 +5,13 @@ */ namespace Magento\Catalog\Model; +use Magento\Authorization\Model\UserContextInterface; use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\AuthorizationInterface; use Magento\Framework\Convert\ConvertArray; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Profiler; @@ -211,6 +214,16 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements */ protected $metadataService; + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -311,6 +324,7 @@ protected function getCustomAttributesCodes() return $this->customAttributesCodes; } + // phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod /** * Returns model resource * @@ -322,6 +336,7 @@ protected function _getResource() { return parent::_getResource(); } + // phpcs:enable /** * Get flat resource model flag @@ -606,11 +621,13 @@ public function getUrl() return $this->getData('url'); } - $rewrite = $this->urlFinder->findOneByData([ - UrlRewrite::ENTITY_ID => $this->getId(), - UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::STORE_ID => $this->getStoreId(), - ]); + $rewrite = $this->urlFinder->findOneByData( + [ + UrlRewrite::ENTITY_ID => $this->getId(), + UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $this->getStoreId(), + ] + ); if ($rewrite) { $this->setData('url', $this->getUrlInstance()->getDirectUrl($rewrite->getRequestPath())); Profiler::stop('REWRITE: ' . __METHOD__); @@ -914,6 +931,60 @@ public function beforeDelete() return parent::beforeDelete(); } + /** + * Get user context. + * + * @return UserContextInterface + */ + private function getUserContext(): UserContextInterface + { + if (!$this->userContext) { + $this->userContext = ObjectManager::getInstance()->get(UserContextInterface::class); + } + + return $this->userContext; + } + + /** + * Get authorization service. + * + * @return AuthorizationInterface + */ + private function getAuthorization(): AuthorizationInterface + { + if (!$this->authorization) { + $this->authorization = ObjectManager::getInstance()->get(AuthorizationInterface::class); + } + + return $this->authorization; + } + + /** + * @inheritDoc + */ + public function beforeSave() + { + //Validate changing of design. + $userType = $this->getUserContext()->getUserType(); + if (( + $userType === UserContextInterface::USER_TYPE_ADMIN + || $userType === UserContextInterface::USER_TYPE_INTEGRATION + ) + && !$this->getAuthorization()->isAllowed('Magento_Catalog::edit_category_design') + ) { + foreach ($this->_designAttributes as $attributeCode) { + $this->setData($attributeCode, $value = $this->getOrigData($attributeCode)); + if (!empty($this->_data[self::CUSTOM_ATTRIBUTES]) + && array_key_exists($attributeCode, $this->_data[self::CUSTOM_ATTRIBUTES])) { + //In case custom attribute were used to update the entity. + $this->_data[self::CUSTOM_ATTRIBUTES][$attributeCode]->setValue($value); + } + } + } + + return parent::beforeSave(); + } + /** * Retrieve anchors above * diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index a4127c9a97ffd..c96b2aae36059 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -25,6 +25,7 @@ use Magento\Ui\Component\Form\Field; use Magento\Ui\DataProvider\EavValidationRules; use Magento\Ui\DataProvider\Modifier\PoolInterface; +use Magento\Framework\AuthorizationInterface; /** * Class DataProvider @@ -32,6 +33,7 @@ * @api * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) * @since 101.0.0 */ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider @@ -146,6 +148,11 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider */ private $fileInfo; + /** + * @var AuthorizationInterface + */ + private $auth; + /** * DataProvider constructor * @@ -162,6 +169,7 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider * @param array $meta * @param array $data * @param PoolInterface|null $pool + * @param AuthorizationInterface|null $auth * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -177,7 +185,8 @@ public function __construct( CategoryFactory $categoryFactory, array $meta = [], array $data = [], - PoolInterface $pool = null + PoolInterface $pool = null, + ?AuthorizationInterface $auth = null ) { $this->eavValidationRules = $eavValidationRules; $this->collection = $categoryCollectionFactory->create(); @@ -187,6 +196,7 @@ public function __construct( $this->storeManager = $storeManager; $this->request = $request; $this->categoryFactory = $categoryFactory; + $this->auth = $auth ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data, $pool); } @@ -210,6 +220,8 @@ public function getMeta() } /** + * Disable fields if they are using default values. + * * @param Category $category * @param array $meta * @return array @@ -260,10 +272,13 @@ private function addUseDefaultValueCheckbox(Category $category, array $meta) */ public function prepareMeta($meta) { - $meta = array_replace_recursive($meta, $this->prepareFieldsMeta( - $this->getFieldsMap(), - $this->getAttributesMeta($this->eavConfig->getEntityType('catalog_category')) - )); + $meta = array_replace_recursive( + $meta, + $this->prepareFieldsMeta( + $this->getFieldsMap(), + $this->getAttributesMeta($this->eavConfig->getEntityType('catalog_category')) + ) + ); return $meta; } @@ -277,11 +292,20 @@ public function prepareMeta($meta) */ private function prepareFieldsMeta($fieldsMap, $fieldsMeta) { + $canEditDesign = $this->auth->isAllowed('Magento_Catalog::edit_category_design'); + $result = []; foreach ($fieldsMap as $fieldSet => $fields) { foreach ($fields as $field) { if (isset($fieldsMeta[$field])) { - $result[$fieldSet]['children'][$field]['arguments']['data']['config'] = $fieldsMeta[$field]; + $config = $fieldsMeta[$field]; + if (($fieldSet === 'design' || $fieldSet === 'schedule_design_update') && !$canEditDesign) { + $config['required'] = 1; + $config['disabled'] = 1; + $config['serviceDisabled'] = true; + } + + $result[$fieldSet]['children'][$field]['arguments']['data']['config'] = $config; } } } @@ -498,6 +522,7 @@ private function convertValues($category, $categoryData) $stat = $fileInfo->getStat($fileName); $mime = $fileInfo->getMimeType($fileName); + // phpcs:ignore Magento2.Functions.DiscouragedFunction $categoryData[$attributeCode][0]['name'] = basename($fileName); if ($fileInfo->isBeginsWithMediaDirectoryPath($fileName)) { @@ -533,6 +558,8 @@ public function getDefaultMetaData($result) } /** + * List of fields groups and fields. + * * @return array * @since 101.0.0 */ diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 7485d9f6cb247..a8636306f5e5b 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -13,6 +13,8 @@ use Magento\Catalog\Api\Data\CategoryInterface; /** + * Repository for categories. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInterface @@ -70,7 +72,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) { @@ -125,7 +127,7 @@ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) } /** - * {@inheritdoc} + * @inheritdoc */ public function get($categoryId, $storeId = null) { @@ -146,7 +148,7 @@ public function get($categoryId, $storeId = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(\Magento\Catalog\Api\Data\CategoryInterface $category) { @@ -167,7 +169,7 @@ public function delete(\Magento\Catalog\Api\Data\CategoryInterface $category) } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteByIdentifier($categoryId) { @@ -208,6 +210,8 @@ protected function validateCategory(Category $category) } /** + * Lazy loader for the converter. + * * @return \Magento\Framework\Api\ExtensibleDataObjectConverter * * @deprecated 101.0.0 @@ -222,6 +226,8 @@ private function getExtensibleDataObjectConverter() } /** + * Lazy loader for the metadata pool. + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 61544f8fb5766..fc9fffb2a7e9a 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -5,6 +5,7 @@ */ namespace Magento\Catalog\Model; +use Magento\Authorization\Model\UserContextInterface; use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; use Magento\Catalog\Api\Data\ProductInterface; @@ -14,6 +15,7 @@ use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\AuthorizationInterface; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Pricing\SaleableInterface; @@ -353,6 +355,16 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements */ private $filterCustomAttribute; + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -485,6 +497,7 @@ protected function _construct() $this->_init(\Magento\Catalog\Model\ResourceModel\Product::class); } + // phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod /** * Get resource instance * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod @@ -497,6 +510,7 @@ protected function _getResource() { return parent::_getResource(); } + // phpcs:enable /** * Get a list of custom attribute codes that belongs to product attribute set. @@ -858,6 +872,34 @@ public function getAttributes($groupId = null, $skipSuper = false) return $attributes; } + /** + * Get user context. + * + * @return UserContextInterface + */ + private function getUserContext(): UserContextInterface + { + if (!$this->userContext) { + $this->userContext = ObjectManager::getInstance()->get(UserContextInterface::class); + } + + return $this->userContext; + } + + /** + * Get authorization service. + * + * @return AuthorizationInterface + */ + private function getAuthorization(): AuthorizationInterface + { + if (!$this->authorization) { + $this->authorization = ObjectManager::getInstance()->get(AuthorizationInterface::class); + } + + return $this->authorization; + } + /** * Check product options and type options and save them, too * @@ -875,6 +917,22 @@ public function beforeSave() $this->getTypeInstance()->beforeSave($this); + //Validate changing of design. + $userType = $this->getUserContext()->getUserType(); + if (( + $userType === UserContextInterface::USER_TYPE_ADMIN + || $userType === UserContextInterface::USER_TYPE_INTEGRATION + ) + && !$this->getAuthorization()->isAllowed('Magento_Catalog::edit_product_design') + ) { + $this->setData('custom_design', $this->getOrigData('custom_design')); + $this->setData('page_layout', $this->getOrigData('page_layout')); + $this->setData('options_container', $this->getOrigData('options_container')); + $this->setData('custom_layout_update', $this->getOrigData('custom_layout_update')); + $this->setData('custom_design_from', $this->getOrigData('custom_design_from')); + $this->setData('custom_design_to', $this->getOrigData('custom_design_to')); + } + $hasOptions = false; $hasRequiredOptions = false; @@ -1167,7 +1225,8 @@ public function getFormattedPrice() /** * Get formatted by currency product price * - * @return array|double* + * @return array|double + * * @deprecated * @see getFormattedPrice() */ diff --git a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php b/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php new file mode 100644 index 0000000000000..404760a51eff5 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Class to check that product is saleable. + */ +class SalabilityChecker +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param ProductRepositoryInterface $productRepository + * @param StoreManagerInterface $storeManager + */ + public function __construct( + ProductRepositoryInterface $productRepository, + StoreManagerInterface $storeManager + ) { + $this->productRepository = $productRepository; + $this->storeManager = $storeManager; + } + + /** + * Check if product is salable. + * + * @param int|string $productId + * @param int|null $storeId + * @return bool + */ + public function isSalable($productId, $storeId = null): bool + { + if ($storeId === null) { + $storeId = $this->storeManager->getStore()->getId(); + } + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->getById($productId, false, $storeId); + + return $product->isSalable(); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 536fda7e093d3..786cec391c460 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -12,8 +12,10 @@ namespace Magento\Catalog\Model\ResourceModel; use Magento\Catalog\Model\Indexer\Category\Product\Processor; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; +use Magento\Catalog\Model\Category as CategoryEntity; /** * Resource model for category entity @@ -90,7 +92,6 @@ class Category extends AbstractResource * @var Processor */ private $indexerProcessor; - /** * Category constructor. * @param \Magento\Eav\Model\Entity\Context $context @@ -102,6 +103,7 @@ class Category extends AbstractResource * @param Processor $indexerProcessor * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Eav\Model\Entity\Context $context, @@ -125,7 +127,7 @@ public function __construct( $this->_eventManager = $eventManager; $this->connectionName = 'catalog'; $this->indexerProcessor = $indexerProcessor; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() + $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); } @@ -1026,7 +1028,7 @@ protected function _processPositions($category, $newParent, $afterCategoryId) if ($afterCategoryId) { $select = $connection->select()->from($table, 'position')->where('entity_id = :entity_id'); $position = $connection->fetchOne($select, ['entity_id' => $afterCategoryId]); - $position += 1; + $position++; } else { $position = 1; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index d56cc40ad0fc2..e7c98b218f5ad 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -193,6 +193,7 @@ public function beforeSave() if ($this->_data[self::KEY_IS_GLOBAL] != $this->_origData[self::KEY_IS_GLOBAL]) { try { $this->attrLockValidator->validate($this); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $exception) { throw new \Magento\Framework\Exception\LocalizedException( __('Do not change the scope. %1', $exception->getMessage()) @@ -845,14 +846,9 @@ public function afterDelete() /** * @inheritdoc * @since 100.0.9 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->unsetData('entity_type'); return array_diff( parent::__sleep(), @@ -863,14 +859,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.9 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_indexerEavProcessor = $objectManager->get(\Magento\Catalog\Model\Indexer\Product\Flat\Processor::class); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index 99a7efe6c9895..b0b15cfd69d13 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -8,6 +8,7 @@ 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\Catalog\Model\Product as ProductEntity; use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\Model\AbstractModel; @@ -607,7 +608,9 @@ public function countAll() } /** - * @inheritdoc + * @inheritDoc + * + * @param ProductEntity|object $object */ public function validate($object) { @@ -689,7 +692,7 @@ public function save(AbstractModel $object) } /** - * Retrieve entity manager object + * Retrieve entity manager. * * @return EntityManager */ @@ -703,7 +706,7 @@ private function getEntityManager() } /** - * Retrieve ProductWebsiteLink object + * Retrieve ProductWebsiteLink instance. * * @deprecated 101.1.0 * @return ProductWebsiteLink @@ -714,7 +717,7 @@ private function getProductWebsiteLink() } /** - * Retrieve CategoryLink object + * Retrieve CategoryLink instance. * * @deprecated 101.1.0 * @return Product\CategoryLink diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml index 044b38a19c4ea..31204c7b4b0bf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml @@ -18,6 +18,9 @@ <testCaseId value="MAGETWO-98644"/> <useCaseId value="MAGETWO-98522"/> <group value="Catalog"/> + <skip> + <issueId value="MC-15930"/> + </skip> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 8cb59b1a2ccec..88075b13f1430 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -266,15 +266,17 @@ protected function setUp() $this->searchResultsMock = $this->getMockBuilder(SearchResultsInterface::class) ->getMockForAbstractClass(); $this->eavAttributeMock = $this->getMockBuilder(Attribute::class) - ->setMethods([ - 'load', - 'getAttributeGroupCode', - 'getApplyTo', - 'getFrontendInput', - 'getAttributeCode', - 'usesSource', - 'getSource', - ]) + ->setMethods( + [ + 'load', + 'getAttributeGroupCode', + 'getApplyTo', + 'getFrontendInput', + 'getAttributeCode', + 'usesSource', + 'getSource', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) @@ -307,9 +309,7 @@ protected function setUp() ->willReturnSelf(); $this->groupCollectionMock->expects($this->any()) ->method('getIterator') - ->willReturn(new \ArrayIterator([ - $this->groupMock, - ])); + ->willReturn(new \ArrayIterator([$this->groupMock])); $this->attributeCollectionMock->expects($this->any()) ->method('addFieldToSelect') ->willReturnSelf(); @@ -324,9 +324,7 @@ protected function setUp() ->willReturn($this->attributeCollectionMock); $this->productMock->expects($this->any()) ->method('getAttributes') - ->willReturn([ - $this->attributeMock, - ]); + ->willReturn([$this->attributeMock,]); $this->storeMock = $this->getMockBuilder(StoreInterface::class) ->setMethods(['load', 'getId', 'getConfig', 'getBaseCurrencyCode']) ->getMockForAbstractClass(); @@ -355,24 +353,27 @@ protected function setUp() */ protected function createModel() { - return $this->objectManager->getObject(Eav::class, [ - 'locator' => $this->locatorMock, - 'eavValidationRules' => $this->eavValidationRulesMock, - 'eavConfig' => $this->eavConfigMock, - 'request' => $this->requestMock, - 'groupCollectionFactory' => $this->groupCollectionFactoryMock, - 'storeManager' => $this->storeManagerMock, - 'formElementMapper' => $this->formElementMapperMock, - 'metaPropertiesMapper' => $this->metaPropertiesMapperMock, - 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, - 'attributeGroupRepository' => $this->attributeGroupRepositoryMock, - 'sortOrderBuilder' => $this->sortOrderBuilderMock, - 'attributeRepository' => $this->attributeRepositoryMock, - 'arrayManager' => $this->arrayManagerMock, - 'eavAttributeFactory' => $this->eavAttributeFactoryMock, - '_eventManager' => $this->eventManagerMock, - 'attributeCollectionFactory' => $this->attributeCollectionFactoryMock - ]); + return $this->objectManager->getObject( + Eav::class, + [ + 'locator' => $this->locatorMock, + 'eavValidationRules' => $this->eavValidationRulesMock, + 'eavConfig' => $this->eavConfigMock, + 'request' => $this->requestMock, + 'groupCollectionFactory' => $this->groupCollectionFactoryMock, + 'storeManager' => $this->storeManagerMock, + 'formElementMapper' => $this->formElementMapperMock, + 'metaPropertiesMapper' => $this->metaPropertiesMapperMock, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + 'attributeGroupRepository' => $this->attributeGroupRepositoryMock, + 'sortOrderBuilder' => $this->sortOrderBuilderMock, + 'attributeRepository' => $this->attributeRepositoryMock, + 'arrayManager' => $this->arrayManagerMock, + 'eavAttributeFactory' => $this->eavAttributeFactoryMock, + '_eventManager' => $this->eventManagerMock, + 'attributeCollectionFactory' => $this->attributeCollectionFactoryMock + ] + ); } public function testModifyData() @@ -389,9 +390,7 @@ public function testModifyData() ->willReturn($this->attributeCollectionMock); $this->attributeCollectionMock->expects($this->any())->method('getItems') - ->willReturn([ - $this->eavAttributeMock - ]); + ->willReturn([$this->eavAttributeMock]); $this->locatorMock->expects($this->any())->method('getProduct') ->willReturn($this->productMock); @@ -480,11 +479,11 @@ public function testSetupAttributeMetaDefaultAttribute( ['value' => ['test1', 'test2'], 'label' => 'Array label'], ]; $attributeOptionsExpected = [ - ['value' => '1', 'label' => 'Int label'], - ['value' => '1.5', 'label' => 'Float label'], - ['value' => '1', 'label' => 'Boolean label'], - ['value' => 'string', 'label' => 'String label'], - ['value' => ['test1', 'test2'], 'label' => 'Array label'], + ['value' => '1', 'label' => 'Int label', '__disableTmpl' => true], + ['value' => '1.5', 'label' => 'Float label', '__disableTmpl' => true], + ['value' => '1', 'label' => 'Boolean label', '__disableTmpl' => true], + ['value' => 'string', 'label' => 'String label', '__disableTmpl' => true], + ['value' => ['test1', 'test2'], 'label' => 'Array label', '__disableTmpl' => true], ]; $this->productMock->method('getId')->willReturn($productId); diff --git a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php index ea6b1fd47a0a5..9a6a22fcb0985 100644 --- a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php @@ -65,18 +65,24 @@ 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() || array_key_exists($columnName, $filterModifiers)) - ? $this->getFilterType($attribute->getFrontendInput()) - : null, - ], $config); + $config = array_merge( + [ + 'label' => __($attribute->getDefaultFrontendLabel()), + 'dataType' => $this->getDataType($attribute), + 'add_field' => true, + 'visible' => $attribute->getIsVisibleInGrid(), + 'filter' => ($attribute->getIsFilterableInGrid() || array_key_exists($columnName, $filterModifiers)) + ? $this->getFilterType($attribute->getFrontendInput()) + : null, + ], + $config + ); if ($attribute->usesSource()) { $config['options'] = $attribute->getSource()->getAllOptions(); + foreach ($config['options'] as &$optionData) { + $optionData['__disableTmpl'] = true; + } } $config['component'] = $this->getJsComponent($config['dataType']); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index 0b8f551988a80..5f1907344ce83 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 @@ -447,6 +447,7 @@ private function retrieveCategoriesTree(int $storeId, array $shownCategoriesIds) $categoryById[$category->getId()]['is_active'] = $category->getIsActive(); $categoryById[$category->getId()]['label'] = $category->getName(); + $categoryById[$category->getId()]['__disableTmpl'] = true; $categoryById[$category->getParentId()]['optgroup'][] = &$categoryById[$category->getId()]; } 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 old mode 100644 new mode 100755 index 8326c3b531892..5d1e853cef3d1 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -21,8 +21,10 @@ use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory as GroupCollectionFactory; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SortOrderBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\App\RequestInterface; +use Magento\Framework\AuthorizationInterface; use Magento\Framework\Filter\Translit; use Magento\Framework\Locale\CurrencyInterface; use Magento\Framework\Stdlib\ArrayManager; @@ -213,6 +215,11 @@ class Eav extends AbstractModifier */ private $scopeConfig; + /** + * @var AuthorizationInterface + */ + private $auth; + /** * Eav constructor. * @param LocatorInterface $locator @@ -237,6 +244,7 @@ class Eav extends AbstractModifier * @param CompositeConfigProcessor|null $wysiwygConfigProcessor * @param ScopeConfigInterface|null $scopeConfig * @param AttributeCollectionFactory $attributeCollectionFactory + * @param AuthorizationInterface|null $auth * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -261,7 +269,8 @@ public function __construct( $attributesToEliminate = [], CompositeConfigProcessor $wysiwygConfigProcessor = null, ScopeConfigInterface $scopeConfig = null, - AttributeCollectionFactory $attributeCollectionFactory = null + AttributeCollectionFactory $attributeCollectionFactory = null, + ?AuthorizationInterface $auth = null ) { $this->locator = $locator; $this->catalogEavValidationRules = $catalogEavValidationRules; @@ -282,12 +291,12 @@ public function __construct( $this->dataPersistor = $dataPersistor; $this->attributesToDisable = $attributesToDisable; $this->attributesToEliminate = $attributesToEliminate; - $this->wysiwygConfigProcessor = $wysiwygConfigProcessor ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(CompositeConfigProcessor::class); - $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(ScopeConfigInterface::class); + $this->wysiwygConfigProcessor = $wysiwygConfigProcessor + ?: ObjectManager::getInstance()->get(CompositeConfigProcessor::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); $this->attributeCollectionFactory = $attributeCollectionFactory - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(AttributeCollectionFactory::class); + ?: ObjectManager::getInstance()->get(AttributeCollectionFactory::class); + $this->auth = $auth ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); } /** @@ -651,6 +660,7 @@ private function isProductExists() * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @api * @since 101.0.0 */ @@ -658,58 +668,65 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC { $configPath = ltrim(static::META_CONFIG_PATH, ArrayManager::DEFAULT_PATH_DELIMITER); $attributeCode = $attribute->getAttributeCode(); - $meta = $this->arrayManager->set($configPath, [], [ - 'dataType' => $attribute->getFrontendInput(), - 'formElement' => $this->getFormElementsMapValue($attribute->getFrontendInput()), - 'visible' => $attribute->getIsVisible(), - 'required' => $attribute->getIsRequired(), - 'notice' => $attribute->getNote() === null ? null : __($attribute->getNote()), - 'default' => (!$this->isProductExists()) ? $this->getAttributeDefaultValue($attribute) : null, - 'label' => __($attribute->getDefaultFrontendLabel()), - 'code' => $attributeCode, - 'source' => $groupCode, - 'scopeLabel' => $this->getScopeLabel($attribute), - 'globalScope' => $this->isScopeGlobal($attribute), - 'sortOrder' => $sortOrder * self::SORT_ORDER_MULTIPLIER, - ]); + $meta = $this->arrayManager->set( + $configPath, + [], + [ + 'dataType' => $attribute->getFrontendInput(), + 'formElement' => $this->getFormElementsMapValue($attribute->getFrontendInput()), + 'visible' => $attribute->getIsVisible(), + 'required' => $attribute->getIsRequired(), + 'notice' => $attribute->getNote() === null ? null : __($attribute->getNote()), + 'default' => (!$this->isProductExists()) ? $this->getAttributeDefaultValue($attribute) : null, + 'label' => __($attribute->getDefaultFrontendLabel()), + 'code' => $attributeCode, + 'source' => $groupCode, + 'scopeLabel' => $this->getScopeLabel($attribute), + 'globalScope' => $this->isScopeGlobal($attribute), + 'sortOrder' => $sortOrder * self::SORT_ORDER_MULTIPLIER, + ] + ); // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done $attributeModel = $this->getAttributeModel($attribute); if ($attributeModel->usesSource()) { $options = $attributeModel->getSource()->getAllOptions(true, true); - $meta = $this->arrayManager->merge($configPath, $meta, [ - 'options' => $this->convertOptionsValueToString($options), - ]); + foreach ($options as &$option) { + $option['__disableTmpl'] = true; + } + $meta = $this->arrayManager->merge( + $configPath, + $meta, + ['options' => $this->convertOptionsValueToString($options)] + ); } if ($this->canDisplayUseDefault($attribute)) { - $meta = $this->arrayManager->merge($configPath, $meta, [ - 'service' => [ - 'template' => 'ui/form/element/helper/service', + $meta = $this->arrayManager->merge( + $configPath, + $meta, + [ + 'service' => [ + 'template' => 'ui/form/element/helper/service', + ] ] - ]); + ); } if (!$this->arrayManager->exists($configPath . '/componentType', $meta)) { - $meta = $this->arrayManager->merge($configPath, $meta, [ - 'componentType' => Field::NAME, - ]); + $meta = $this->arrayManager->merge($configPath, $meta, ['componentType' => Field::NAME]); } $product = $this->locator->getProduct(); if (in_array($attributeCode, $this->attributesToDisable) || $product->isLockedAttribute($attributeCode)) { - $meta = $this->arrayManager->merge($configPath, $meta, [ - 'disabled' => true, - ]); + $meta = $this->arrayManager->merge($configPath, $meta, ['disabled' => true]); } // TODO: getAttributeModel() should not be used when MAGETWO-48284 is complete $childData = $this->arrayManager->get($configPath, $meta, []); if (($rules = $this->catalogEavValidationRules->build($this->getAttributeModel($attribute), $childData))) { - $meta = $this->arrayManager->merge($configPath, $meta, [ - 'validation' => $rules, - ]); + $meta = $this->arrayManager->merge($configPath, $meta, ['validation' => $rules]); } $meta = $this->addUseDefaultValueCheckbox($attribute, $meta); @@ -730,6 +747,23 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC break; } + //Checking access to design config. + $designDesignGroups = ['design', 'schedule-design-update']; + if (in_array($groupCode, $designDesignGroups, true)) { + if (!$this->auth->isAllowed('Magento_Catalog::edit_product_design')) { + $meta = $this->arrayManager->merge( + $configPath, + $meta, + [ + 'disabled' => true, + 'validation' => ['required' => false], + 'required' => false, + 'serviceDisabled' => true, + ] + ); + } + } + return $meta; } @@ -760,11 +794,14 @@ private function getAttributeDefaultValue(ProductAttributeInterface $attribute) */ private function convertOptionsValueToString(array $options) : array { - array_walk($options, function (&$value) { - if (isset($value['value']) && is_scalar($value['value'])) { - $value['value'] = (string)$value['value']; + array_walk( + $options, + function (&$value) { + if (isset($value['value']) && is_scalar($value['value'])) { + $value['value'] = (string)$value['value']; + } } - }); + ); return $options; } diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 5c3ee3da8ca81..fc22bf3438e3f 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -31,7 +31,8 @@ "magento/module-ui": "*", "magento/module-url-rewrite": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/module-wishlist": "*", + "magento/module-authorization": "*" }, "suggest": { "magento/module-cookie": "*", diff --git a/app/code/Magento/Catalog/etc/acl.xml b/app/code/Magento/Catalog/etc/acl.xml index 4d4b7bdc672d1..c7c0f1f75872d 100644 --- a/app/code/Magento/Catalog/etc/acl.xml +++ b/app/code/Magento/Catalog/etc/acl.xml @@ -12,9 +12,12 @@ <resource id="Magento_Catalog::catalog" title="Catalog" translate="title" sortOrder="30"> <resource id="Magento_Catalog::catalog_inventory" title="Inventory" translate="title" sortOrder="10"> <resource id="Magento_Catalog::products" title="Products" translate="title" sortOrder="10"> - <resource id="Magento_Catalog::update_attributes" title="Update Attributes" translate="title" /> + <resource id="Magento_Catalog::update_attributes" title="Update Attributes" translate="title" sortOrder="10" /> + <resource id="Magento_Catalog::edit_product_design" title="Edit Product Design" translate="title" sortOrder="20" /> + </resource> + <resource id="Magento_Catalog::categories" title="Categories" translate="title" sortOrder="20"> + <resource id="Magento_Catalog::edit_category_design" title="Edit Category Design" translate="title" /> </resource> - <resource id="Magento_Catalog::categories" title="Categories" translate="title" sortOrder="20" /> </resource> </resource> <resource id="Magento_Backend::stores"> diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index ed27dfd646cb2..c35ffbce1a125 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -808,4 +808,6 @@ Details,Details "Start typing to find products", "Start typing to find products" "Product with ID: (%1) doesn't exist", "Product with ID: (%1) doesn't exist" "Category with ID: (%1) doesn't exist", "Category with ID: (%1) doesn't exist" -"You added product %1 to the <a href=""%2"">comparison list</a>.","You added product %1 to the <a href=""%2"">comparison list</a>." \ No newline at end of file +"You added product %1 to the <a href=""%2"">comparison list</a>.","You added product %1 to the <a href=""%2"">comparison list</a>." +"Edit Product Design","Edit Product Design" +"Edit Category Design","Edit Category Design" \ No newline at end of file diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml index 90d6e0b48400e..1ce9a669f0ee6 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml @@ -466,7 +466,7 @@ <dataType>string</dataType> <label translate="true">Theme</label> <imports> - <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked</link> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> </field> @@ -475,7 +475,7 @@ <dataType>string</dataType> <label translate="true">Layout</label> <imports> - <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked</link> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> </field> @@ -484,7 +484,7 @@ <dataType>string</dataType> <label translate="true">Layout Update XML</label> <imports> - <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked</link> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> </field> @@ -501,7 +501,7 @@ <dataType>boolean</dataType> <label translate="true">Apply Design to Products</label> <imports> - <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked</link> + <link name="serviceDisabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> <formElements> @@ -544,7 +544,7 @@ <dataType>string</dataType> <label translate="true">Schedule Update From</label> <imports> - <link name="disabled">ns = ${ $.ns }, index = custom_use_parent_settings :checked</link> + <link name="disabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> </field> @@ -557,7 +557,7 @@ <dataType>string</dataType> <label translate="true">To</label> <imports> - <link name="disabled">ns = ${ $.ns }, index = custom_use_parent_settings :checked</link> + <link name="disabled">${ $.parentName }.custom_use_parent_settings:checked || $.data.serviceDisabled</link> </imports> </settings> </field> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml index 2e022a5df14ed..a045a21e55d27 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/attribute.phtml @@ -15,6 +15,11 @@ <?php $_helper = $this->helper(Magento\Catalog\Helper\Output::class); $_product = $block->getProduct(); + +if (!$_product instanceof \Magento\Catalog\Model\Product) { + return; +} + $_call = $block->getAtCall(); $_code = $block->getAtCode(); $_className = $block->getCssClass(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php index f587be245c99d..67ca3b85d6f2f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php @@ -48,7 +48,7 @@ public function calculate(int $rootCategoryId) : int $connection = $this->resourceConnection->getConnection(); $select = $connection->select() ->from($this->resourceConnection->getTableName('catalog_category_entity'), 'level') - ->where($this->resourceCategory->getLinkField() . " = ?", $rootCategoryId); + ->where($this->resourceCategory->getEntityIdField() . " = ?", $rootCategoryId); return (int) $connection->fetchOne($select); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php index aba2d7b198dbd..dd18c463b98de 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php @@ -14,6 +14,9 @@ */ class CategoriesIdentity implements IdentityInterface { + /** @var string */ + private $cacheTag = \Magento\Catalog\Model\Category::CACHE_TAG; + /** * Get category IDs from resolved data * @@ -25,7 +28,10 @@ public function getIdentities(array $resolvedData): array $ids = []; if (!empty($resolvedData)) { foreach ($resolvedData as $category) { - $ids[] = $category['id']; + $ids[] = sprintf('%s_%s', $this->cacheTag, $category['id']); + } + if (!empty($ids)) { + array_unshift($ids, $this->cacheTag); } } return $ids; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php index e4970f08b3eb7..017a7d280c195 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php @@ -14,6 +14,9 @@ */ class CategoryTreeIdentity implements IdentityInterface { + /** @var string */ + private $cacheTag = \Magento\Catalog\Model\Category::CACHE_TAG; + /** * Get category ID from resolved data * @@ -22,6 +25,7 @@ class CategoryTreeIdentity implements IdentityInterface */ public function getIdentities(array $resolvedData): array { - return empty($resolvedData['id']) ? [] : [$resolvedData['id']]; + return empty($resolvedData['id']) ? + [] : [$this->cacheTag, sprintf('%s_%s', $this->cacheTag, $resolvedData['id'])]; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php index 198b1c112dca2..7aec66ccb699f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php @@ -14,6 +14,9 @@ */ class Identity implements IdentityInterface { + /** @var string */ + private $cacheTag = \Magento\Catalog\Model\Product::CACHE_TAG; + /** * Get product ids for cache tag * @@ -25,7 +28,10 @@ public function getIdentities(array $resolvedData): array $ids = []; $items = $resolvedData['items'] ?? []; foreach ($items as $item) { - $ids[] = $item['entity_id']; + $ids[] = sprintf('%s_%s', $this->cacheTag, $item['entity_id']); + } + if (!empty($ids)) { + array_unshift($ids, $this->cacheTag); } return $ids; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/MediaGalleryProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/MediaGalleryProcessor.php new file mode 100644 index 0000000000000..be300e11f12ec --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/MediaGalleryProcessor.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Catalog\Model\Product\Media\Config as MediaConfig; + +/** + * Add attributes required for every GraphQL product resolution process. + * + * {@inheritdoc} + */ +class MediaGalleryProcessor implements CollectionProcessorInterface +{ + /** + * @var MediaConfig + */ + private $mediaConfig; + + /** + * Add media gallery attributes to collection + * + * @param MediaConfig $mediaConfig + */ + public function __construct(MediaConfig $mediaConfig) + { + $this->mediaConfig = $mediaConfig; + } + + /** + * @inheritdoc + */ + public function process( + Collection $collection, + SearchCriteriaInterface $searchCriteria, + array $attributeNames + ): Collection { + if (in_array('media_gallery_entries', $attributeNames)) { + $mediaAttributes = $this->mediaConfig->getMediaAttributeCodes(); + foreach ($mediaAttributes as $mediaAttribute) { + if (!in_array($mediaAttribute, $attributeNames)) { + $collection->addAttributeToSelect($mediaAttribute); + } + } + } + + return $collection; + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 406d37b2ea200..a5006355ed265 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -46,6 +46,7 @@ <item name="search" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\SearchCriteriaProcessor</item> <item name="stock" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\StockProcessor</item> <item name="visibility" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\VisibilityStatusProcessor</item> + <item name="mediaGallery" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\MediaGalleryProcessor</item> </argument> </arguments> </type> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index f4d0990b17049..bbc01ac0854c0 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -8,12 +8,12 @@ type Query { pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") - ): Products - @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheTag: "cat_p", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") + ): Products + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( id: Int @doc(description: "Id of the category.") ): CategoryTree - @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @cache(cacheTag: "cat_c", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") } type Price @doc(description: "The Price object defines the price of a product as well as any tax-related adjustments.") { @@ -97,7 +97,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ price: ProductPrices @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") gift_message_available: String @doc(description: "Indicates whether a gift message is available.") manufacturer: Int @doc(description: "A number representing the product's manufacturer.") - categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @cache(cacheTag: "cat_c", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") + categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") canonical_url: String @doc(description: "Canonical URL.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") } @@ -218,7 +218,7 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") - ): CategoryProducts @doc(description: "The list of products assigned to the category.") @cache(cacheTag: "cat_p", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") + ): CategoryProducts @doc(description: "The list of products assigned to the category.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") } @@ -233,7 +233,7 @@ type CustomizableRadioOption implements CustomizableOptionInterface @doc(descrip value: [CustomizableRadioValue] @doc(description: "An array that defines a set of radio buttons.") } -type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines the price and sku of a product whose page contains a customized set of radio buttons.") { +type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines the price and sku of a product whose page contains a customized set of radio buttons.") { option_type_id: Int @doc(description: "The ID assigned to the value.") price: Float @doc(description: "The price assigned to this option.") price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") @@ -246,7 +246,7 @@ type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(desc value: [CustomizableCheckboxValue] @doc(description: "An array that defines a set of checkbox values.") } -type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defines the price and sku of a product whose page contains a customized set of checkbox values.") { +type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defines the price and sku of a product whose page contains a customized set of checkbox values.") { option_type_id: Int @doc(description: "The ID assigned to the value.") price: Float @doc(description: "The price assigned to this option.") price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") @@ -329,7 +329,7 @@ type ProductMediaGalleryEntriesVideoContent @doc(description: "ProductMediaGalle video_metadata: String @doc(description: "Optional data about the video.") } -input ProductSortInput @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { +input ProductSortInput @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { name: SortEnum @doc(description: "The product name. Customers use this name to identify the product.") sku: SortEnum @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: SortEnum @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @@ -363,7 +363,7 @@ input ProductSortInput @doc(description: "ProductSortInput specifies the attrib gift_message_available: SortEnum @doc(description: "Indicates whether a gift message is available.") } -type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { +type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { id: Int @doc(description: "The identifier assigned to the object.") media_type: String @doc(description: "image or video.") label: String @doc(description: "The alt text displayed on the UI when the user points to the image.") diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index 3ac7f98818d70..4ce1c0e39d6de 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -6,6 +6,7 @@ namespace Magento\CatalogImportExport\Model\Import; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Filesystem\DriverPool; /** @@ -13,6 +14,8 @@ * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Functions.DiscouragedFunction */ class Uploader extends \Magento\MediaStorage\Model\File\Uploader { @@ -31,6 +34,13 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader */ protected $_tmpDir = ''; + /** + * Download directory for url-based resources. + * + * @var string + */ + private $downloadDir; + /** * Destination directory. * @@ -94,6 +104,13 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader */ protected $_coreFileStorage; + /** + * Instance of random data generator. + * + * @var \Magento\Framework\Math\Random + */ + private $random; + /** * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb * @param \Magento\MediaStorage\Helper\File\Storage $coreFileStorage @@ -102,6 +119,8 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Framework\Filesystem\File\ReadFactory $readFactory * @param string|null $filePath + * @param \Magento\Framework\Math\Random|null $random + * @throws \Magento\Framework\Exception\FileSystemException * @throws \Magento\Framework\Exception\LocalizedException */ public function __construct( @@ -111,7 +130,8 @@ public function __construct( \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $validator, \Magento\Framework\Filesystem $filesystem, \Magento\Framework\Filesystem\File\ReadFactory $readFactory, - $filePath = null + $filePath = null, + \Magento\Framework\Math\Random $random = null ) { $this->_imageFactory = $imageFactory; $this->_coreFileStorageDb = $coreFileStorageDb; @@ -122,6 +142,8 @@ public function __construct( if ($filePath !== null) { $this->_setUploadFile($filePath); } + $this->random = $random ?: ObjectManager::getInstance()->get(\Magento\Framework\Math\Random::class); + $this->downloadDir = DirectoryList::getDefaultConfig()[DirectoryList::TMP][DirectoryList::PATH]; } /** @@ -150,52 +172,61 @@ public function init() */ public function move($fileName, $renameFileOff = false) { - if ($renameFileOff) { - $this->setAllowRenameFiles(false); - } - - if ($this->getTmpDir()) { - $filePath = $this->getTmpDir() . '/'; - } else { - $filePath = ''; - } + $this->setAllowRenameFiles(!$renameFileOff); if (preg_match('/\bhttps?:\/\//i', $fileName, $matches)) { $url = str_replace($matches[0], '', $fileName); - $driver = $matches[0] === $this->httpScheme ? DriverPool::HTTP : DriverPool::HTTPS; - $read = $this->_readFactory->create($url, $driver); - - //only use filename (for URI with query parameters) - $parsedUrlPath = parse_url($url, PHP_URL_PATH); - if ($parsedUrlPath) { - $urlPathValues = explode('/', $parsedUrlPath); - if (!empty($urlPathValues)) { - $fileName = end($urlPathValues); - } - } - - $fileExtension = pathinfo($fileName, PATHINFO_EXTENSION); - if ($fileExtension && !$this->checkAllowedExtension($fileExtension)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Disallowed file type.')); - } - - $fileName = preg_replace('/[^a-z0-9\._-]+/i', '', $fileName); - $relativePath = $this->_directory->getRelativePath($filePath . $fileName); - $this->_directory->writeFile( - $relativePath, - $read->readAll() - ); + $driver = ($matches[0] === $this->httpScheme) ? DriverPool::HTTP : DriverPool::HTTPS; + $tmpFilePath = $this->downloadFileFromUrl($url, $driver); + } else { + $tmpDir = $this->getTmpDir() ? ($this->getTmpDir() . '/') : ''; + $tmpFilePath = $this->_directory->getRelativePath($tmpDir . $fileName); } - $filePath = $this->_directory->getRelativePath($filePath . $fileName); - $this->_setUploadFile($filePath); + $this->_setUploadFile($tmpFilePath); $destDir = $this->_directory->getAbsolutePath($this->getDestDir()); $result = $this->save($destDir); unset($result['path']); $result['name'] = self::getCorrectFileName($result['name']); + return $result; } + /** + * Writes a url-based file to the temp directory. + * + * @param string $url + * @param string $driver + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function downloadFileFromUrl($url, $driver) + { + $parsedUrlPath = parse_url($url, PHP_URL_PATH); + if (!$parsedUrlPath) { + throw new \Magento\Framework\Exception\LocalizedException(__('Could not parse resource url.')); + } + $urlPathValues = explode('/', $parsedUrlPath); + $fileName = preg_replace('/[^a-z0-9\._-]+/i', '', end($urlPathValues)); + + $fileExtension = pathinfo($fileName, PATHINFO_EXTENSION); + if ($fileExtension && !$this->checkAllowedExtension($fileExtension)) { + throw new \Magento\Framework\Exception\LocalizedException(__('Disallowed file type.')); + } + + $tmpFileName = str_replace(".$fileExtension", '', $fileName); + $tmpFileName .= '_' . $this->random->getRandomString(16); + $tmpFileName .= $fileExtension ? ".$fileExtension" : ''; + $tmpFilePath = $this->_directory->getRelativePath($this->downloadDir . '/' . $tmpFileName); + + $this->_directory->writeFile( + $tmpFilePath, + $this->_readFactory->create($url, $driver)->readAll() + ); + + return $tmpFilePath; + } + /** * Prepare information about the file for moving * @@ -238,7 +269,7 @@ protected function _readFileInfo($filePath) * Validate uploaded file by type and etc. * * @return void - * @throws \Exception + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _validateFile() { @@ -251,8 +282,7 @@ protected function _validateFile() $fileExtension = pathinfo($filePath, PATHINFO_EXTENSION); if (!$this->checkAllowedExtension($fileExtension)) { - $this->_directory->delete($filePath); - throw new \Exception('Disallowed file type.'); + throw new \Magento\Framework\Exception\LocalizedException(__('Disallowed file type.')); } //run validate callbacks foreach ($this->_validateCallbacks as $params) { @@ -356,6 +386,7 @@ protected function _moveFile($tmpPath, $destPath) */ protected function chmod($file) { + //phpcs:ignore Squiz.PHP.NonExecutableCode.ReturnNotRequired return; } } diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml index 65588daa96cc4..911453cf7b049 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml @@ -54,6 +54,7 @@ <argument name="rowIndex" type="string"/> </arguments> <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad time="30" stepKey="waitFormReload"/> <click stepKey="clickSelectBtn" selector="{{AdminExportAttributeSection.selectByIndex(rowIndex)}}"/> <click stepKey="clickOnDelete" selector="{{AdminExportAttributeSection.delete(rowIndex)}}" after="clickSelectBtn"/> <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml index 3b8da4055ab7e..ec46b09808e14 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml @@ -118,7 +118,6 @@ <!-- Go to export page --> <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> - <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> <!-- Fill entity attributes data --> <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php index f734596de014b..2c6aa6535c10e 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php @@ -6,6 +6,11 @@ */ namespace Magento\CatalogImportExport\Test\Unit\Model\Import; +/** + * Class UploaderTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class UploaderTest extends \PHPUnit\Framework\TestCase { /** @@ -43,6 +48,11 @@ class UploaderTest extends \PHPUnit\Framework\TestCase */ protected $directoryMock; + /** + * @var \Magento\Framework\Math\Random|\PHPUnit_Framework_MockObject_MockObject + */ + private $random; + /** * @var \Magento\CatalogImportExport\Model\Import\Uploader|\PHPUnit_Framework_MockObject_MockObject */ @@ -84,56 +94,90 @@ protected function setUp() ->method('getDirectoryWrite') ->will($this->returnValue($this->directoryMock)); + $this->random = $this->getMockBuilder(\Magento\Framework\Math\Random::class) + ->disableOriginalConstructor() + ->setMethods(['getRandomString']) + ->getMock(); + $this->uploader = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Uploader::class) - ->setConstructorArgs([ - $this->coreFileStorageDb, - $this->coreFileStorage, - $this->imageFactory, - $this->validator, - $this->filesystem, - $this->readFactory, - ]) + ->setConstructorArgs( + [ + $this->coreFileStorageDb, + $this->coreFileStorage, + $this->imageFactory, + $this->validator, + $this->filesystem, + $this->readFactory, + null, + $this->random + ] + ) ->setMethods(['_setUploadFile', 'save', 'getTmpDir', 'checkAllowedExtension']) ->getMock(); } /** * @dataProvider moveFileUrlDataProvider + * @param $fileUrl + * @param $expectedHost + * @param $expectedFileName + * @param $checkAllowedExtension + * @throws \Magento\Framework\Exception\LocalizedException */ public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName, $checkAllowedExtension) { + $tmpDir = 'var/tmp'; $destDir = 'var/dest/dir'; - $expectedRelativeFilePath = $expectedFileName; - $this->directoryMock->expects($this->once())->method('isWritable')->with($destDir)->willReturn(true); - $this->directoryMock->expects($this->any())->method('getRelativePath')->with($expectedRelativeFilePath); - $this->directoryMock->expects($this->once())->method('getAbsolutePath')->with($destDir) - ->willReturn($destDir . '/' . $expectedFileName); - // Check writeFile() method invoking. - $this->directoryMock->expects($this->any())->method('writeFile')->will($this->returnValue($expectedFileName)); + + // Expected invocation to validate file extension + $this->uploader->expects($this->exactly($checkAllowedExtension))->method('checkAllowedExtension') + ->willReturn(true); + + // Expected invocation to generate random string for file name postfix + $this->random->expects($this->once())->method('getRandomString') + ->with(16) + ->willReturn('38GcEmPFKXXR8NMj'); + + // Expected invocation to build the temp file path with the correct directory and filename + $this->directoryMock->expects($this->any())->method('getRelativePath') + ->with($tmpDir . '/' . $expectedFileName); // Create adjusted reader which does not validate path. $readMock = $this->getMockBuilder(\Magento\Framework\Filesystem\File\Read::class) ->disableOriginalConstructor() ->setMethods(['readAll']) ->getMock(); - // Check readAll() method invoking. - $readMock->expects($this->once())->method('readAll')->will($this->returnValue(null)); - // Check create() method invoking with expected argument. - $this->readFactory->expects($this->once()) - ->method('create') - ->will($this->returnValue($readMock))->with($expectedHost); - //Check invoking of getTmpDir(), _setUploadFile(), save() methods. - $this->uploader->expects($this->any())->method('getTmpDir')->will($this->returnValue('')); - $this->uploader->expects($this->once())->method('_setUploadFile')->will($this->returnSelf()); - $this->uploader->expects($this->once())->method('save')->with($destDir . '/' . $expectedFileName) - ->willReturn(['name' => $expectedFileName, 'path' => 'absPath']); - $this->uploader->expects($this->exactly($checkAllowedExtension)) - ->method('checkAllowedExtension') + // Expected invocations to create reader and read contents from url + $this->readFactory->expects($this->once())->method('create') + ->with($expectedHost) + ->will($this->returnValue($readMock)); + $readMock->expects($this->once())->method('readAll') + ->will($this->returnValue(null)); + + // Expected invocation to write the temp file + $this->directoryMock->expects($this->any())->method('writeFile') + ->will($this->returnValue($expectedFileName)); + + // Expected invocations to move the temp file to the destination directory + $this->directoryMock->expects($this->once())->method('isWritable') + ->with($destDir) ->willReturn(true); + $this->directoryMock->expects($this->once())->method('getAbsolutePath') + ->with($destDir) + ->willReturn($destDir . '/' . $expectedFileName); + $this->uploader->expects($this->once())->method('_setUploadFile') + ->willReturnSelf(); + $this->uploader->expects($this->once())->method('save') + ->with($destDir . '/' . $expectedFileName) + ->willReturn(['name' => $expectedFileName, 'path' => 'absPath']); + + // Do not use configured temp directory + $this->uploader->expects($this->never())->method('getTmpDir'); $this->uploader->setDestDir($destDir); $result = $this->uploader->move($fileUrl); + $this->assertEquals(['name' => $expectedFileName], $result); $this->assertArrayNotHasKey('path', $result); } @@ -182,14 +226,16 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive ->willReturn($driverMock); $uploaderMock = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Uploader::class) - ->setConstructorArgs([ - $this->coreFileStorageDb, - $this->coreFileStorage, - $this->imageFactory, - $this->validator, - $this->filesystem, - $readFactory, - ]) + ->setConstructorArgs( + [ + $this->coreFileStorageDb, + $this->coreFileStorage, + $this->imageFactory, + $this->validator, + $this->filesystem, + $readFactory, + ] + ) ->getMock(); $result = $uploaderMock->move($fileUrl); @@ -223,42 +269,66 @@ public function moveFileUrlDriverPoolDataProvider() public function moveFileUrlDataProvider() { return [ - [ - '$fileUrl' => 'http://test_uploader_file', + 'https_no_file_ext' => [ + '$fileUrl' => 'https://test_uploader_file', '$expectedHost' => 'test_uploader_file', - '$expectedFileName' => 'test_uploader_file', + '$expectedFileName' => 'test_uploader_file_38GcEmPFKXXR8NMj', '$checkAllowedExtension' => 0 ], - [ - '$fileUrl' => 'https://!:^&`;file', - '$expectedHost' => '!:^&`;file', - '$expectedFileName' => 'file', + 'https_invalid_chars' => [ + '$fileUrl' => 'https://www.google.com/!:^&`;image.jpg', + '$expectedHost' => 'www.google.com/!:^&`;image.jpg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', + '$checkAllowedExtension' => 1 + ], + 'https_invalid_chars_no_file_ext' => [ + '$fileUrl' => 'https://!:^&`;image', + '$expectedHost' => '!:^&`;image', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj', '$checkAllowedExtension' => 0 ], - [ + 'http_jpg' => [ + '$fileUrl' => 'http://www.google.com/image.jpg', + '$expectedHost' => 'www.google.com/image.jpg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', + '$checkAllowedExtension' => 1 + ], + 'https_jpg' => [ '$fileUrl' => 'https://www.google.com/image.jpg', '$expectedHost' => 'www.google.com/image.jpg', - '$expectedFileName' => 'image.jpg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', '$checkAllowedExtension' => 1 ], - [ + 'https_jpeg' => [ + '$fileUrl' => 'https://www.google.com/image.jpeg', + '$expectedHost' => 'www.google.com/image.jpeg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpeg', + '$checkAllowedExtension' => 1 + ], + 'https_png' => [ + '$fileUrl' => 'https://www.google.com/image.png', + '$expectedHost' => 'www.google.com/image.png', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.png', + '$checkAllowedExtension' => 1 + ], + 'https_gif' => [ + '$fileUrl' => 'https://www.google.com/image.gif', + '$expectedHost' => 'www.google.com/image.gif', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.gif', + '$checkAllowedExtension' => 1 + ], + 'https_one_query_param' => [ '$fileUrl' => 'https://www.google.com/image.jpg?param=1', '$expectedHost' => 'www.google.com/image.jpg?param=1', - '$expectedFileName' => 'image.jpg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', '$checkAllowedExtension' => 1 ], - [ + 'https_two_query_params' => [ '$fileUrl' => 'https://www.google.com/image.jpg?param=1¶m=2', '$expectedHost' => 'www.google.com/image.jpg?param=1¶m=2', - '$expectedFileName' => 'image.jpg', - '$checkAllowedExtension' => 1 - ], - [ - '$fileUrl' => 'http://www.google.com/image.jpg?param=1¶m=2', - '$expectedHost' => 'www.google.com/image.jpg?param=1¶m=2', - '$expectedFileName' => 'image.jpg', + '$expectedFileName' => 'image_38GcEmPFKXXR8NMj.jpg', '$checkAllowedExtension' => 1 - ], + ] ]; } } diff --git a/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php b/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php index 0a02d4eb6a9a6..6f3e40b622f42 100644 --- a/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php +++ b/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php @@ -7,8 +7,6 @@ namespace Magento\CatalogInventory\Model; use Magento\Catalog\Model\ResourceModel\Product\Collection; -use Magento\Framework\Search\EngineResolverInterface; -use Magento\Search\Model\EngineResolver; /** * Catalog inventory module plugin @@ -20,21 +18,13 @@ class AddStockStatusToCollection */ protected $stockHelper; - /** - * @var EngineResolverInterface - */ - private $engineResolver; - /** * @param \Magento\CatalogInventory\Helper\Stock $stockHelper - * @param EngineResolverInterface $engineResolver */ public function __construct( - \Magento\CatalogInventory\Helper\Stock $stockHelper, - EngineResolverInterface $engineResolver + \Magento\CatalogInventory\Helper\Stock $stockHelper ) { $this->stockHelper = $stockHelper; - $this->engineResolver = $engineResolver; } /** @@ -47,9 +37,7 @@ public function __construct( */ public function beforeLoad(Collection $productCollection, $printQuery = false, $logQuery = false) { - if ($this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE) { - $this->stockHelper->addIsInStockFilterToCollection($productCollection); - } + $this->stockHelper->addIsInStockFilterToCollection($productCollection); return [$printQuery, $logQuery]; } } diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php b/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php deleted file mode 100644 index 168e947b8fa57..0000000000000 --- a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php +++ /dev/null @@ -1,92 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\CatalogInventory\Model\Plugin; - -use Magento\Framework\Search\EngineResolverInterface; -use Magento\Search\Model\EngineResolver; - -/** - * Catalog inventory plugin for layer. - */ -class Layer -{ - /** - * Stock status instance - * - * @var \Magento\CatalogInventory\Helper\Stock - */ - protected $stockHelper; - - /** - * Store config instance - * - * @var \Magento\Framework\App\Config\ScopeConfigInterface - */ - protected $scopeConfig; - - /** - * @var EngineResolverInterface - */ - private $engineResolver; - - /** - * @param \Magento\CatalogInventory\Helper\Stock $stockHelper - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param EngineResolverInterface $engineResolver - */ - public function __construct( - \Magento\CatalogInventory\Helper\Stock $stockHelper, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - EngineResolverInterface $engineResolver - ) { - $this->stockHelper = $stockHelper; - $this->scopeConfig = $scopeConfig; - $this->engineResolver = $engineResolver; - } - - /** - * Before prepare product collection handler - * - * @param \Magento\Catalog\Model\Layer $subject - * @param \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection $collection - * - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforePrepareProductCollection( - \Magento\Catalog\Model\Layer $subject, - \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection $collection - ) { - if (!$this->isCurrentEngineMysql() || $this->_isEnabledShowOutOfStock()) { - return; - } - $this->stockHelper->addIsInStockFilterToCollection($collection); - } - - /** - * Check if current engine is MYSQL. - * - * @return bool - */ - private function isCurrentEngineMysql() - { - return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; - } - - /** - * Get config value for 'display out of stock' option - * - * @return bool - */ - protected function _isEnabledShowOutOfStock() - { - return $this->scopeConfig->isSetFlag( - 'cataloginventory/options/show_out_of_stock', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - } -} diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php deleted file mode 100644 index b64563a35176d..0000000000000 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/LayerTest.php +++ /dev/null @@ -1,106 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\CatalogInventory\Test\Unit\Model\Plugin; - -use Magento\Framework\Search\EngineResolverInterface; - -class LayerTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\CatalogInventory\Model\Plugin\Layer - */ - protected $_model; - - /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_scopeConfigMock; - - /** - * @var \Magento\CatalogInventory\Helper\Stock|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_stockHelperMock; - - /** - * @var EngineResolverInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $engineResolver; - - protected function setUp() - { - $this->_scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - $this->_stockHelperMock = $this->createMock(\Magento\CatalogInventory\Helper\Stock::class); - $this->engineResolver = $this->getMockBuilder(EngineResolverInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getCurrentSearchEngine']) - ->getMockForAbstractClass(); - - $this->_model = new \Magento\CatalogInventory\Model\Plugin\Layer( - $this->_stockHelperMock, - $this->_scopeConfigMock, - $this->engineResolver - ); - } - - /** - * Test add stock status to collection with disabled 'display out of stock' option - */ - public function testAddStockStatusDisabledShow() - { - $this->engineResolver->expects($this->any()) - ->method('getCurrentSearchEngine') - ->willReturn('mysql'); - - $this->_scopeConfigMock->expects( - $this->once() - )->method( - 'isSetFlag' - )->with( - 'cataloginventory/options/show_out_of_stock' - )->will( - $this->returnValue(true) - ); - /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collectionMock */ - $collectionMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); - $this->_stockHelperMock->expects($this->never())->method('addIsInStockFilterToCollection'); - /** @var \Magento\Catalog\Model\Layer $subjectMock */ - $subjectMock = $this->createMock(\Magento\Catalog\Model\Layer::class); - $this->_model->beforePrepareProductCollection($subjectMock, $collectionMock); - } - - /** - * Test add stock status to collection with 'display out of stock' option enabled - */ - public function testAddStockStatusEnabledShow() - { - $this->engineResolver->expects($this->any()) - ->method('getCurrentSearchEngine') - ->willReturn('mysql'); - - $this->_scopeConfigMock->expects( - $this->once() - )->method( - 'isSetFlag' - )->with( - 'cataloginventory/options/show_out_of_stock' - )->will( - $this->returnValue(false) - ); - - $collectionMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); - - $this->_stockHelperMock->expects( - $this->once() - )->method( - 'addIsInStockFilterToCollection' - )->with( - $collectionMock - ); - - $subjectMock = $this->createMock(\Magento\Catalog\Model\Layer::class); - $this->_model->beforePrepareProductCollection($subjectMock, $collectionMock); - } -} diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json index eb6239ea87ef0..007d744b2296f 100644 --- a/app/code/Magento/CatalogInventory/composer.json +++ b/app/code/Magento/CatalogInventory/composer.json @@ -8,7 +8,6 @@ "php": "~7.1.3||~7.2.0", "magento/framework": "*", "magento/module-catalog": "*", - "magento/module-search": "*", "magento/module-config": "*", "magento/module-customer": "*", "magento/module-eav": "*", diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index e7d79c593b8c7..78a0c2b734315 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -47,9 +47,6 @@ <argument name="resourceStockItem" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Item\Proxy</argument> </arguments> </type> - <type name="Magento\Catalog\Model\Layer"> - <plugin name="addStockStatusOnPrepareFrontCollection" type="Magento\CatalogInventory\Model\Plugin\Layer"/> - </type> <type name="Magento\Framework\Module\Setup\Migration"> <arguments> <argument name="compositeModules" xsi:type="array"> diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php index 0ff12faf54cbf..4f58293d53359 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php @@ -97,6 +97,9 @@ public function execute() unset($data['rule']); } + unset($data['conditions_serialized']); + unset($data['actions_serialized']); + $model->loadPost($data); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setPageData($data); diff --git a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php index 6e963ea1aa8ac..8527ef56c509b 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced/ProductCollectionPrepareStrategyProvider.php @@ -42,7 +42,7 @@ public function __construct( public function getStrategy(): ProductCollectionPrepareStrategyInterface { if (!isset($this->strategies[$this->engineResolver->getCurrentSearchEngine()])) { - throw new \DomainException('Undefined strategy ' . $this->engineResolver->getCurrentSearchEngine()); + return $this->strategies['default']; } return $this->strategies[$this->engineResolver->getCurrentSearchEngine()]; } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index cd2529a8fd725..09d4f0068459a 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -7,9 +7,14 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterface; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Select; use Magento\Store\Model\Store; +use Magento\Framework\App\ObjectManager; /** * Catalog search full test search data provider. @@ -124,6 +129,16 @@ class DataProvider */ private $antiGapMultiplier; + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var StockStatusRepositoryInterface + */ + private $stockStatusRepository; + /** * @param ResourceConnection $resource * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -548,6 +563,8 @@ public function prepareProductIndex($indexData, $productData, $storeId) { $index = []; + $indexData = $this->filterOutOfStockProducts($indexData, $storeId); + foreach ($this->getSearchableAttributes('static') as $attribute) { $attributeCode = $attribute->getAttributeCode(); @@ -672,4 +689,68 @@ private function filterAttributeValue($value) { return preg_replace('/\s+/iu', ' ', trim(strip_tags($value))); } + + /** + * Filter out of stock products for products. + * + * @param array $indexData + * @param int $storeId + * @return array + */ + private function filterOutOfStockProducts($indexData, $storeId): array + { + if (!$this->getStockConfiguration()->isShowOutOfStock($storeId)) { + $productIds = array_keys($indexData); + $stockStatusCriteria = $this->createStockStatusCriteria(); + $stockStatusCriteria->setProductsFilter($productIds); + $stockStatusCollection = $this->getStockStatusRepository()->getList($stockStatusCriteria); + $stockStatuses = $stockStatusCollection->getItems(); + $stockStatuses = array_filter( + $stockStatuses, + function (StockStatusInterface $stockStatus) { + return StockStatusInterface::STATUS_IN_STOCK == $stockStatus->getStockStatus(); + } + ); + $indexData = array_intersect_key($indexData, $stockStatuses); + } + return $indexData; + } + + /** + * Get stock configuration. + * + * @return StockConfigurationInterface + */ + private function getStockConfiguration() + { + if (null === $this->stockConfiguration) { + $this->stockConfiguration = ObjectManager::getInstance()->get(StockConfigurationInterface::class); + } + return $this->stockConfiguration; + } + + /** + * Create stock status criteria. + * + * Substitution of autogenerated factory in backward compatibility reasons. + * + * @return StockStatusCriteriaInterface + */ + private function createStockStatusCriteria() + { + return ObjectManager::getInstance()->create(StockStatusCriteriaInterface::class); + } + + /** + * Get stock status repository. + * + * @return StockStatusRepositoryInterface + */ + private function getStockStatusRepository() + { + if (null === $this->stockStatusRepository) { + $this->stockStatusRepository = ObjectManager::getInstance()->get(StockStatusRepositoryInterface::class); + } + return $this->stockStatusRepository; + } } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 9bd9a895a2af9..1946dd35b8d37 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -7,10 +7,11 @@ namespace Magento\CatalogSearch\Model\ResourceModel\Advanced; use Magento\Catalog\Model\Product; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyChecker; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyCheckerInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; use Magento\Framework\Search\EngineResolverInterface; -use Magento\Search\Model\EngineResolver; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; use Magento\Framework\Api\FilterBuilder; @@ -106,6 +107,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $searchOrders; + /** + * @var DefaultFilterStrategyApplyCheckerInterface + */ + private $defaultFilterStrategyApplyChecker; + /** * Collection constructor * @@ -140,6 +146,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param SearchResultApplierFactory|null $searchResultApplierFactory * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory * @param EngineResolverInterface|null $engineResolver + * @param DefaultFilterStrategyApplyCheckerInterface|null $defaultFilterStrategyApplyChecker * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -173,7 +180,8 @@ public function __construct( SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, SearchResultApplierFactory $searchResultApplierFactory = null, TotalRecordsResolverFactory $totalRecordsResolverFactory = null, - EngineResolverInterface $engineResolver = null + EngineResolverInterface $engineResolver = null, + DefaultFilterStrategyApplyCheckerInterface $defaultFilterStrategyApplyChecker = null ) { $this->requestBuilder = $requestBuilder; $this->searchEngine = $searchEngine; @@ -191,6 +199,8 @@ public function __construct( ->get(TotalRecordsResolverFactory::class); $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() ->get(EngineResolverInterface::class); + $this->defaultFilterStrategyApplyChecker = $defaultFilterStrategyApplyChecker ?: ObjectManager::getInstance() + ->get(DefaultFilterStrategyApplyChecker::class); parent::__construct( $entityFactory, $logger, @@ -237,8 +247,12 @@ public function addFieldsToFilter($fields) */ public function setOrder($attribute, $dir = Select::SQL_DESC) { + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ $this->setSearchOrder($attribute, $dir); - if ($this->isCurrentEngineMysql()) { + if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::setOrder($attribute, $dir); } @@ -254,7 +268,7 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) * This changes need in backward compatible reasons for support dynamic improved algorithm * for price aggregation process. */ - if ($this->isCurrentEngineMysql()) { + if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::addCategoryFilter($category); } else { $this->addFieldToFilter('category_ids', $category->getId()); @@ -273,7 +287,7 @@ public function setVisibility($visibility) * This changes need in backward compatible reasons for support dynamic improved algorithm * for price aggregation process. */ - if ($this->isCurrentEngineMysql()) { + if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::setVisibility($visibility); } else { $this->addFieldToFilter('visibility', $visibility); @@ -297,16 +311,6 @@ private function setSearchOrder($field, $direction) $this->searchOrders[$field] = $direction; } - /** - * Check if current engine is MYSQL. - * - * @return bool - */ - private function isCurrentEngineMysql() - { - return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; - } - /** * @inheritdoc */ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 7e4b4f764f64b..4f84f3868c6a3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -6,13 +6,14 @@ namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyChecker; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyCheckerInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; -use Magento\Framework\Search\EngineResolverInterface; use Magento\Framework\Data\Collection\Db\SizeResolverInterfaceFactory; use Magento\Framework\DB\Select; use Magento\Framework\Api\Search\SearchResultInterface; @@ -26,7 +27,6 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; -use Magento\Search\Model\EngineResolver; /** * Fulltext Collection @@ -124,14 +124,14 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection private $totalRecordsResolverFactory; /** - * @var EngineResolverInterface + * @var array */ - private $engineResolver; + private $searchOrders; /** - * @var array + * @var DefaultFilterStrategyApplyCheckerInterface */ - private $searchOrders; + private $defaultFilterStrategyApplyChecker; /** * Collection constructor @@ -170,7 +170,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param SearchCriteriaResolverFactory|null $searchCriteriaResolverFactory * @param SearchResultApplierFactory|null $searchResultApplierFactory * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory - * @param EngineResolverInterface|null $engineResolver + * @param DefaultFilterStrategyApplyCheckerInterface|null $defaultFilterStrategyApplyChecker * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -209,7 +209,7 @@ public function __construct( SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, SearchResultApplierFactory $searchResultApplierFactory = null, TotalRecordsResolverFactory $totalRecordsResolverFactory = null, - EngineResolverInterface $engineResolver = null + DefaultFilterStrategyApplyCheckerInterface $defaultFilterStrategyApplyChecker = null ) { $this->queryFactory = $catalogSearchData; if ($searchResultFactory === null) { @@ -255,8 +255,8 @@ public function __construct( ->get(SearchResultApplierFactory::class); $this->totalRecordsResolverFactory = $totalRecordsResolverFactory ?: ObjectManager::getInstance() ->get(TotalRecordsResolverFactory::class); - $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() - ->get(EngineResolverInterface::class); + $this->defaultFilterStrategyApplyChecker = $defaultFilterStrategyApplyChecker ?: ObjectManager::getInstance() + ->get(DefaultFilterStrategyApplyChecker::class); } /** @@ -393,13 +393,31 @@ public function addSearchFilter($query) public function setOrder($attribute, $dir = Select::SQL_DESC) { $this->setSearchOrder($attribute, $dir); - if ($this->isCurrentEngineMysql()) { + if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::setOrder($attribute, $dir); } return $this; } + /** + * Add attribute to sort order. + * + * @param string $attribute + * @param string $dir + * @return $this + */ + public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) + { + if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { + parent::addAttributeToSort($attribute, $dir); + } else { + $this->setOrder($attribute, $dir); + } + + return $this; + } + /** * @inheritdoc */ @@ -443,16 +461,6 @@ private function setSearchOrder($field, $direction) $this->searchOrders[$field] = $direction; } - /** - * Check if current engine is MYSQL. - * - * @return bool - */ - private function isCurrentEngineMysql() - { - return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; - } - /** * Get total records resolver. * @@ -580,7 +588,7 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) * This changes need in backward compatible reasons for support dynamic improved algorithm * for price aggregation process. */ - if ($this->isCurrentEngineMysql()) { + if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::addCategoryFilter($category); } else { $this->_productLimitationPrice(); @@ -602,7 +610,7 @@ public function setVisibility($visibility) * This changes need in backward compatible reasons for support dynamic improved algorithm * for price aggregation process. */ - if ($this->isCurrentEngineMysql()) { + if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::setVisibility($visibility); } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php new file mode 100644 index 0000000000000..b396437fc66c7 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +/** + * This class add in backward compatibility purposes to check if need to apply old strategy for filter prepare process. + * @deprecated + */ +class DefaultFilterStrategyApplyChecker implements DefaultFilterStrategyApplyCheckerInterface +{ + /** + * Check if this strategy applicable for current engine. + * + * @return bool + */ + public function isApplicable(): bool + { + return true; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php new file mode 100644 index 0000000000000..a067767775393 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; + +/** + * Added in backward compatibility purposes to check if need to apply old strategy for filter prepare process. + * @deprecated + */ +interface DefaultFilterStrategyApplyCheckerInterface +{ + /** + * Check if this strategy applicable for current engine. + * + * @return bool + */ + public function isApplicable(): bool; +} diff --git a/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php index f621bcbf91835..af19b46f64209 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Search/ItemCollectionProvider.php @@ -43,7 +43,7 @@ public function __construct( public function getCollection(): Collection { if (!isset($this->factories[$this->engineResolver->getCurrentSearchEngine()])) { - throw new \DomainException('Undefined factory ' . $this->engineResolver->getCurrentSearchEngine()); + return $this->factories['default']; } return $this->factories[$this->engineResolver->getCurrentSearchEngine()]->create(); } diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index 7359bd6b454b9..28d5035308dee 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -200,6 +200,7 @@ <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> <arguments> <argument name="factories" xsi:type="array"> + <item name="default" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</item> <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</item> </argument> </arguments> @@ -207,6 +208,7 @@ <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> <arguments> <argument name="strategies" xsi:type="array"> + <item name="default" xsi:type="object">Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategy</item> <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategy</item> </argument> </arguments> diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CatalogUrlResolverIdentity.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CatalogUrlResolverIdentity.php new file mode 100644 index 0000000000000..204080488488a --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CatalogUrlResolverIdentity.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver\UrlRewrite; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Get ids from catalog url rewrite + */ +class CatalogUrlResolverIdentity implements IdentityInterface +{ + /** @var string */ + private $categoryCacheTag = \Magento\Catalog\Model\Category::CACHE_TAG; + + /** @var string */ + private $productCacheTag = \Magento\Catalog\Model\Product::CACHE_TAG; + + /** + * Get identities cache ID from a catalog url rewrite entities + * + * @param array $resolvedData + * @return string[] + */ + public function getIdentities(array $resolvedData): array + { + $ids = []; + if (isset($resolvedData['id'])) { + $selectedCacheTag = isset($resolvedData['type']) ? + $this->getTagFromEntityType($resolvedData['type']) : ''; + if (!empty($selectedCacheTag)) { + $ids = [$selectedCacheTag, sprintf('%s_%s', $selectedCacheTag, $resolvedData['id'])]; + } + } + return $ids; + } + + /** + * Match tag to entity type + * + * @param string $entityType + * @return string + */ + private function getTagFromEntityType(string $entityType) : string + { + $selectedCacheTag = ''; + $type = strtolower($entityType); + switch ($type) { + case 'product': + $selectedCacheTag = $this->productCacheTag; + break; + case 'category': + $selectedCacheTag = $this->categoryCacheTag; + break; + } + return $selectedCacheTag; + } +} diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json index ab91f745c5d0c..4a1a17f1398d6 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json @@ -4,6 +4,7 @@ "type": "magento2-module", "require": { "php": "~7.1.3||~7.2.0", + "magento/module-catalog": "*", "magento/framework": "*" }, "suggest": { diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml new file mode 100644 index 0000000000000..20e6b7e9c0053 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml @@ -0,0 +1,17 @@ +<?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\UrlRewriteGraphQl\Model\Resolver\UrlRewrite\UrlResolverIdentity"> + <arguments> + <argument name="urlResolverIdentities" xsi:type="array"> + <item name="product" xsi:type="object">Magento\CatalogUrlRewriteGraphQl\Model\Resolver\UrlRewrite\CatalogUrlResolverIdentity</item> + <item name="category" xsi:type="object">Magento\CatalogUrlRewriteGraphQl\Model\Resolver\UrlRewrite\CatalogUrlResolverIdentity</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/Checkout/Block/Cart/Coupon.php b/app/code/Magento/Checkout/Block/Cart/Coupon.php index e485eaf64c832..acf3c0922f3c9 100644 --- a/app/code/Magento/Checkout/Block/Cart/Coupon.php +++ b/app/code/Magento/Checkout/Block/Cart/Coupon.php @@ -5,7 +5,11 @@ */ namespace Magento\Checkout\Block\Cart; +use Magento\Captcha\Block\Captcha; + /** + * Block with apply-coupon form. + * * @api */ class Coupon extends \Magento\Checkout\Block\Cart\AbstractCart @@ -28,6 +32,8 @@ public function __construct( } /** + * Applied code. + * * @return string * @codeCoverageIgnore */ @@ -35,4 +41,26 @@ public function getCouponCode() { return $this->getQuote()->getCouponCode(); } + + /** + * @inheritDoc + */ + protected function _prepareLayout() + { + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => 'sales_rule_coupon_request', + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + + return parent::_prepareLayout(); + } } diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index 31513d25a9ce1..6dfdefb8601aa 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -6,12 +6,17 @@ namespace Magento\Checkout\Model; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\App\ObjectManager; use Magento\Quote\Model\Quote; use Magento\Quote\Model\QuoteIdMaskFactory; +use Psr\Log\LoggerInterface; /** + * Represents the session data for the checkout process + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Session extends \Magento\Framework\Session\SessionManager { @@ -98,6 +103,11 @@ class Session extends \Magento\Framework\Session\SessionManager */ protected $quoteFactory; + /** + * @var LoggerInterface|null + */ + private $logger; + /** * @param \Magento\Framework\App\Request\Http $request * @param \Magento\Framework\Session\SidResolverInterface $sidResolver @@ -117,6 +127,8 @@ class Session extends \Magento\Framework\Session\SessionManager * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param QuoteIdMaskFactory $quoteIdMaskFactory * @param \Magento\Quote\Model\QuoteFactory $quoteFactory + * @param LoggerInterface|null $logger + * @throws \Magento\Framework\Exception\SessionException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -137,7 +149,8 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, QuoteIdMaskFactory $quoteIdMaskFactory, - \Magento\Quote\Model\QuoteFactory $quoteFactory + \Magento\Quote\Model\QuoteFactory $quoteFactory, + LoggerInterface $logger = null ) { $this->_orderFactory = $orderFactory; $this->_customerSession = $customerSession; @@ -159,6 +172,8 @@ public function __construct( $cookieMetadataFactory, $appState ); + $this->logger = $logger ?: ObjectManager::getInstance() + ->get(LoggerInterface::class); } /** @@ -202,6 +217,8 @@ public function setLoadInactive($load = true) * Get checkout quote instance by current session * * @return Quote + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -219,6 +236,15 @@ public function getQuote() $quote = $this->quoteRepository->getActive($this->getQuoteId()); } + $customerId = $this->_customer + ? $this->_customer->getId() + : $this->_customerSession->getCustomerId(); + + if ($quote->getData('customer_id') && (int)$quote->getData('customer_id') !== (int)$customerId) { + $quote = $this->quoteFactory->create(); + $this->setQuoteId(null); + } + /** * If current currency code of quote is not equal current currency code of store, * need recalculate totals of quote. It is possible if customer use currency switcher or @@ -247,6 +273,7 @@ public function getQuote() $quote = $this->quoteRepository->getActiveForCustomer($customerId); $this->setQuoteId($quote->getId()); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->logger->critical($e); } } else { $quote->setIsCheckoutCart(true); @@ -285,6 +312,8 @@ public function getQuote() } /** + * Return the quote's key + * * @return string * @codeCoverageIgnore */ @@ -294,6 +323,8 @@ protected function _getQuoteIdKey() } /** + * Set the current session's quote id + * * @param int $quoteId * @return void * @codeCoverageIgnore @@ -304,6 +335,8 @@ public function setQuoteId($quoteId) } /** + * Return the current quote's ID + * * @return int * @codeCoverageIgnore */ @@ -357,6 +390,8 @@ public function loadCustomerQuote() } /** + * Associate data to a specified step of the checkout process + * * @param string $step * @param array|string $data * @param bool|string|null $value @@ -383,6 +418,8 @@ public function setStepData($step, $data, $value = null) } /** + * Return the data associated to a specified step + * * @param string|null $step * @param string|null $data * @return array|string|bool @@ -406,8 +443,7 @@ public function getStepData($step = null, $data = null) } /** - * Destroy/end a session - * Unset all data associated with object + * Destroy/end a session and unset all data associated with it * * @return $this */ @@ -443,6 +479,8 @@ public function clearHelperData() } /** + * Revert the state of the checkout to the beginning + * * @return $this * @codeCoverageIgnore */ @@ -453,6 +491,8 @@ public function resetCheckout() } /** + * Replace the quote in the session with a specified object + * * @param Quote $quote * @return $this */ @@ -499,13 +539,17 @@ public function restoreQuote() $this->_eventManager->dispatch('restore_quote', ['order' => $order, 'quote' => $quote]); return true; } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->logger->critical($e); } } + return false; } /** - * @param $isQuoteMasked bool + * Flag whether or not the quote uses a masked quote id + * + * @param bool $isQuoteMasked * @return void * @codeCoverageIgnore */ @@ -515,6 +559,8 @@ protected function setIsQuoteMasked($isQuoteMasked) } /** + * Return if the quote has a masked quote id + * * @return bool|null * @codeCoverageIgnore */ diff --git a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php index 499e02d681f9c..369ae8e6f725e 100644 --- a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php +++ b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php @@ -53,7 +53,6 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf /** * @var QuoteAddressValidator - * @deprecated 100.2.0 */ protected $addressValidator; @@ -152,35 +151,36 @@ public function saveAddressInformation( $cartId, \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation ) { - $address = $addressInformation->getShippingAddress(); - $billingAddress = $addressInformation->getBillingAddress(); - $carrierCode = $addressInformation->getShippingCarrierCode(); - $methodCode = $addressInformation->getShippingMethodCode(); + /** @var \Magento\Quote\Model\Quote $quote */ + $quote = $this->quoteRepository->getActive($cartId); + $this->validateQuote($quote); + $address = $addressInformation->getShippingAddress(); + if (!$address || !$address->getCountryId()) { + throw new StateException(__('The shipping address is missing. Set the address and try again.')); + } if (!$address->getCustomerAddressId()) { $address->setCustomerAddressId(null); } - if ($billingAddress && !$billingAddress->getCustomerAddressId()) { - $billingAddress->setCustomerAddressId(null); - } - - if (!$address->getCountryId()) { - throw new StateException(__('The shipping address is missing. Set the address and try again.')); - } + try { + $billingAddress = $addressInformation->getBillingAddress(); + if ($billingAddress) { + if (!$billingAddress->getCustomerAddressId()) { + $billingAddress->setCustomerAddressId(null); + } + $this->addressValidator->validateForCart($quote, $billingAddress); + $quote->setBillingAddress($billingAddress); + } - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->quoteRepository->getActive($cartId); - $address->setLimitCarrier($carrierCode); - $quote = $this->prepareShippingAssignment($quote, $address, $carrierCode . '_' . $methodCode); - $this->validateQuote($quote); - $quote->setIsMultiShipping(false); + $this->addressValidator->validateForCart($quote, $address); + $carrierCode = $addressInformation->getShippingCarrierCode(); + $address->setLimitCarrier($carrierCode); + $methodCode = $addressInformation->getShippingMethodCode(); + $quote = $this->prepareShippingAssignment($quote, $address, $carrierCode . '_' . $methodCode); - if ($billingAddress) { - $quote->setBillingAddress($billingAddress); - } + $quote->setIsMultiShipping(false); - try { $this->quoteRepository->save($quote); } catch (\Exception $e) { $this->logger->critical($e); diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml index e9beacc3f85a7..c39fab8a52e8e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml @@ -9,7 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CheckoutShippingGuestInfoSection"> - <element name="email" type="input" selector="#checkout-customer-email"/> + <element name="email" type="input" selector="#customer-email"/> <element name="firstName" type="input" selector="input[name=firstname]"/> <element name="lastName" type="input" selector="input[name=lastname]"/> <element name="company" type="input" selector="input[name=company]"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml index 9772fa1993acb..5a3c309c6a1d4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml @@ -11,7 +11,7 @@ <section name="StorefrontCheckoutCheckoutCustomerLoginSection"> <element name="email" type="input" selector="form[data-role='email-with-possible-login'] input[name='username']" /> <element name="emailNoteMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'note')]" /> - <element name="emailErrorMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[@id='checkout-customer-email-error']" /> + <element name="emailErrorMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[@id='customer-email-error']" /> <element name="emailTooltipButton" type="button" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'action-help')]" /> <element name="emailTooltipContent" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'field-tooltip-content')]" /> <element name="password" type="input" selector="form[data-role='email-with-possible-login'] input[name='password']" /> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml new file mode 100644 index 0000000000000..cbf0072d44aed --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectCheckout"> + <annotations> + <features value="Checkout"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Checkout Pages"/> + <description value="Verify that the Secure URL configuration applies to the Checkout pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15531"/> + <group value="checkout"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="_defaultProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="moveMouseOverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="clickAddToCartButton"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForAddedToCartSuccessMessage"/> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$product.name$$ to your shopping cart." stepKey="seeAddedToCartSuccessMessage"/> + <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/checkout" stepKey="goToUnsecureCheckoutURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/checkout" stepKey="seeSecureCheckoutURL"/> + <amOnUrl url="http://{$hostname}/checkout/sidebar" stepKey="goToUnsecureCheckoutSidebarURL"/> + <seeCurrentUrlEquals url="http://{$hostname}/checkout/sidebar" stepKey="seeUnsecureCheckoutSidebarURL"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php deleted file mode 100644 index 1cf5006c20f73..0000000000000 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Cart/CouponPostTest.php +++ /dev/null @@ -1,398 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Checkout\Test\Unit\Controller\Cart; - -use Magento\Checkout\Controller\Cart\Index; - -/** - * Class IndexTest - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class CouponPostTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var Index - */ - protected $controller; - - /** - * @var \Magento\Checkout\Model\Session | \PHPUnit_Framework_MockObject_MockObject - */ - protected $checkoutSession; - - /** - * @var \Magento\Framework\App\Request\Http | \PHPUnit_Framework_MockObject_MockObject - */ - protected $request; - - /** - * @var \Magento\Framework\App\Response\Http | \PHPUnit_Framework_MockObject_MockObject - */ - protected $response; - - /** - * @var \Magento\Quote\Model\Quote | \PHPUnit_Framework_MockObject_MockObject - */ - protected $quote; - - /** - * @var \Magento\Framework\Event\Manager | \PHPUnit_Framework_MockObject_MockObject - */ - protected $eventManager; - - /** - * @var \Magento\Framework\Event\Manager | \PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $cart; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $couponFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteRepository; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $redirect; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $redirectFactory; - - /** - * @return void - */ - protected function setUp() - { - $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); - $this->response = $this->createMock(\Magento\Framework\App\Response\Http::class); - $this->quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, [ - 'setCouponCode', - 'getItemsCount', - 'getShippingAddress', - 'setCollectShippingRates', - 'getCouponCode', - 'collectTotals', - 'save' - ]); - $this->eventManager = $this->createMock(\Magento\Framework\Event\Manager::class); - $this->checkoutSession = $this->createMock(\Magento\Checkout\Model\Session::class); - - $this->objectManagerMock = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, [ - 'get', 'escapeHtml' - ]); - - $this->messageManager = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $context = $this->createMock(\Magento\Framework\App\Action\Context::class); - $context->expects($this->once()) - ->method('getObjectManager') - ->willReturn($this->objectManagerMock); - $context->expects($this->once()) - ->method('getRequest') - ->willReturn($this->request); - $context->expects($this->once()) - ->method('getResponse') - ->willReturn($this->response); - $context->expects($this->once()) - ->method('getEventManager') - ->willReturn($this->eventManager); - $context->expects($this->once()) - ->method('getMessageManager') - ->willReturn($this->messageManager); - - $this->redirectFactory = - $this->createMock(\Magento\Framework\Controller\Result\RedirectFactory::class); - $this->redirect = $this->createMock(\Magento\Store\App\Response\Redirect::class); - - $this->redirect->expects($this->any()) - ->method('getRefererUrl') - ->willReturn(null); - - $context->expects($this->once()) - ->method('getRedirect') - ->willReturn($this->redirect); - - $context->expects($this->once()) - ->method('getResultRedirectFactory') - ->willReturn($this->redirectFactory); - - $this->cart = $this->getMockBuilder(\Magento\Checkout\Model\Cart::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->couponFactory = $this->getMockBuilder(\Magento\SalesRule\Model\CouponFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->quoteRepository = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); - - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->controller = $objectManagerHelper->getObject( - \Magento\Checkout\Controller\Cart\CouponPost::class, - [ - 'context' => $context, - 'checkoutSession' => $this->checkoutSession, - 'cart' => $this->cart, - 'couponFactory' => $this->couponFactory, - 'quoteRepository' => $this->quoteRepository - ] - ); - } - - 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->cart->expects($this->once()) - ->method('getQuote') - ->willReturn($this->quote); - - $this->controller->execute(); - } - - 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->cart->expects($this->any()) - ->method('getQuote') - ->willReturn($this->quote); - - $this->quote->expects($this->at(0)) - ->method('getCouponCode') - ->willReturn('OLDCODE'); - - $coupon = $this->createMock(\Magento\SalesRule\Model\Coupon::class); - $this->couponFactory->expects($this->once()) - ->method('create') - ->willReturn($coupon); - $coupon->expects($this->once())->method('load')->willReturnSelf(); - $coupon->expects($this->once())->method('getId')->willReturn(1); - $this->quote->expects($this->any()) - ->method('getItemsCount') - ->willReturn(1); - - $shippingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - - $this->quote->expects($this->any()) - ->method('setCollectShippingRates') - ->with(true); - - $this->quote->expects($this->any()) - ->method('getShippingAddress') - ->willReturn($shippingAddress); - - $this->quote->expects($this->any()) - ->method('collectTotals') - ->willReturn($this->quote); - - $this->quote->expects($this->any()) - ->method('setCouponCode') - ->with('CODE') - ->willReturnSelf(); - - $this->quote->expects($this->any()) - ->method('getCouponCode') - ->willReturn('CODE'); - - $this->messageManager->expects($this->once()) - ->method('addSuccessMessage') - ->willReturnSelf(); - - $this->objectManagerMock->expects($this->once()) - ->method('get') - ->willReturnSelf(); - - $this->controller->execute(); - } - - 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->cart->expects($this->any()) - ->method('getQuote') - ->willReturn($this->quote); - - $this->quote->expects($this->at(0)) - ->method('getCouponCode') - ->willReturn('OLDCODE'); - - $this->quote->expects($this->any()) - ->method('getItemsCount') - ->willReturn(0); - - $coupon = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - - $coupon->expects($this->once()) - ->method('getId') - ->willReturn(1); - - $this->couponFactory->expects($this->once()) - ->method('create') - ->willReturn($coupon); - - $this->checkoutSession->expects($this->once()) - ->method('getQuote') - ->willReturn($this->quote); - - $this->quote->expects($this->any()) - ->method('setCouponCode') - ->with('CODE') - ->willReturnSelf(); - - $this->messageManager->expects($this->once()) - ->method('addSuccessMessage') - ->willReturnSelf(); - - $this->objectManagerMock->expects($this->once()) - ->method('get') - ->willReturnSelf(); - - $this->controller->execute(); - } - - 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->cart->expects($this->any()) - ->method('getQuote') - ->willReturn($this->quote); - - $this->quote->expects($this->at(0)) - ->method('getCouponCode') - ->willReturn('OLDCODE'); - - $this->quote->expects($this->any()) - ->method('getItemsCount') - ->willReturn(1); - - $shippingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - - $this->quote->expects($this->any()) - ->method('setCollectShippingRates') - ->with(true); - - $this->quote->expects($this->any()) - ->method('getShippingAddress') - ->willReturn($shippingAddress); - - $this->quote->expects($this->any()) - ->method('collectTotals') - ->willReturn($this->quote); - - $this->quote->expects($this->any()) - ->method('setCouponCode') - ->with('') - ->willReturnSelf(); - - $this->messageManager->expects($this->once()) - ->method('addSuccessMessage') - ->with('You canceled the coupon code.') - ->willReturnSelf(); - - $this->controller->execute(); - } - - 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->cart->expects($this->any()) - ->method('getQuote') - ->willReturn($this->quote); - - $this->quote->expects($this->at(0)) - ->method('getCouponCode') - ->willReturn('OLDCODE'); - - $this->quote->expects($this->any()) - ->method('getItemsCount') - ->willReturn(0); - - $coupon = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - - $coupon->expects($this->once()) - ->method('getId') - ->willReturn(0); - - $this->couponFactory->expects($this->once()) - ->method('create') - ->willReturn($coupon); - - $this->messageManager->expects($this->once()) - ->method('addErrorMessage') - ->willReturnSelf(); - - $this->objectManagerMock->expects($this->once()) - ->method('get') - ->willReturnSelf(); - - $this->controller->execute(); - } -} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/ShippingInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/ShippingInformationManagementTest.php index dd88b7161acdf..93375bb884535 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/ShippingInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/ShippingInformationManagementTest.php @@ -82,6 +82,11 @@ class ShippingInformationManagementTest extends \PHPUnit\Framework\TestCase */ private $shippingMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $addressValidatorMock; + protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -141,6 +146,9 @@ protected function setUp() $this->createPartialMock(\Magento\Quote\Api\Data\CartExtensionFactory::class, ['create']); $this->shippingFactoryMock = $this->createPartialMock(\Magento\Quote\Model\ShippingFactory::class, ['create']); + $this->addressValidatorMock = $this->createMock( + \Magento\Quote\Model\QuoteAddressValidator::class + ); $this->model = $this->objectManager->getObject( \Magento\Checkout\Model\ShippingInformationManagement::class, @@ -151,7 +159,8 @@ protected function setUp() 'quoteRepository' => $this->quoteRepositoryMock, 'shippingAssignmentFactory' => $this->shippingAssignmentFactoryMock, 'cartExtensionFactory' => $this->cartExtensionFactoryMock, - 'shippingFactory' => $this->shippingFactoryMock + 'shippingFactory' => $this->shippingFactoryMock, + 'addressValidator' => $this->addressValidatorMock, ] ); } @@ -163,22 +172,8 @@ protected function setUp() public function testSaveAddressInformationIfCartIsEmpty() { $cartId = 100; - $carrierCode = 'carrier_code'; - $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); - $billingAddress = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $addressInformationMock->expects($this->once()) - ->method('getShippingAddress') - ->willReturn($this->shippingAddressMock); - $addressInformationMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddress); - $addressInformationMock->expects($this->once())->method('getShippingCarrierCode')->willReturn($carrierCode); - $addressInformationMock->expects($this->once())->method('getShippingMethodCode')->willReturn($shippingMethod); - - $this->shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn('USA'); - - $this->setShippingAssignmentsMocks($carrierCode . '_' . $shippingMethod); - $this->quoteMock->expects($this->once())->method('getItemsCount')->willReturn(0); $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') @@ -244,21 +239,19 @@ private function setShippingAssignmentsMocks($shippingMethod) public function testSaveAddressInformationIfShippingAddressNotSet() { $cartId = 100; - $carrierCode = 'carrier_code'; - $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); - $addressInformationMock->expects($this->once()) ->method('getShippingAddress') ->willReturn($this->shippingAddressMock); - $addressInformationMock->expects($this->once())->method('getShippingCarrierCode')->willReturn($carrierCode); - $addressInformationMock->expects($this->once())->method('getShippingMethodCode')->willReturn($shippingMethod); - - $billingAddress = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $addressInformationMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddress); $this->shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn(null); + $this->quoteRepositoryMock->expects($this->once()) + ->method('getActive') + ->with($cartId) + ->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getItemsCount')->willReturn(100); + $this->model->saveAddressInformation($cartId, $addressInformationMock); } @@ -273,6 +266,9 @@ public function testSaveAddressInformationIfCanNotSaveQuote() $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); + $this->addressValidatorMock->expects($this->exactly(2)) + ->method('validateForCart'); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') ->with($cartId) @@ -314,6 +310,9 @@ public function testSaveAddressInformationIfCarrierCodeIsInvalid() $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); + $this->addressValidatorMock->expects($this->exactly(2)) + ->method('validateForCart'); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') ->with($cartId) @@ -355,6 +354,9 @@ public function testSaveAddressInformation() $shippingMethod = 'shipping_method'; $addressInformationMock = $this->createMock(\Magento\Checkout\Api\Data\ShippingInformationInterface::class); + $this->addressValidatorMock->expects($this->exactly(2)) + ->method('validateForCart'); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive') ->with($cartId) diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index 540565345bd9b..4051a9cc512ff 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -23,7 +23,8 @@ "magento/module-store": "*", "magento/module-tax": "*", "magento/module-theme": "*", - "magento/module-ui": "*" + "magento/module-ui": "*", + "magento/module-captcha": "*" }, "suggest": { "magento/module-cookie": "*" diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index e1ba4381f2230..f8c2e7ebcb503 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -33,5 +33,21 @@ <template>checkout_payment_failed_template</template> </payment_failed> </checkout> + <captcha> + <frontend> + <areas> + <sales_rule_coupon_request> + <label>Applying coupon code</label> + </sales_rule_coupon_request> + </areas> + </frontend> + </captcha> + <customer> + <captcha> + <shown_to_logged_in_user> + <sales_rule_coupon_request>1</sales_rule_coupon_request> + </shown_to_logged_in_user> + </captcha> + </customer> </default> </config> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml index 4522500d395b6..bf8490affea0c 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/coupon.phtml @@ -4,6 +4,9 @@ * See COPYING.txt for license details. */ +/** + * @var \Magento\Framework\View\Element\AbstractBlock $block + */ ?> <div class="block discount" id="block-discount" @@ -51,6 +54,9 @@ <?php endif; ?> </div> </div> + <?php if (!strlen($block->getCouponCode())) : ?> + <?= /* @noEscape */ $block->getChildHtml('captcha') ?> + <?php endif; ?> </form> </div> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html index 5b8dde81dd93e..4afaf3c89a5e0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html @@ -31,7 +31,7 @@ <div class="block block-customer-login" data-bind="attr: {'data-label': $t('or')}"> <div class="block-title"> - <strong id="block-customer-login-heading-checkout" + <strong id="block-customer-login-heading" role="heading" aria-level="2" data-bind="i18n: 'Sign In'"></strong> @@ -39,7 +39,7 @@ <!-- ko foreach: getRegion('messages') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> - <div class="block-content" aria-labelledby="block-customer-login-heading-checkout"> + <div class="block-content" aria-labelledby="block-customer-login-heading"> <form data-role="login" data-bind="submit:login" method="post"> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html index 6a784fa7a04c4..8d6142e07fcf0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html @@ -14,7 +14,7 @@ method="post"> <fieldset id="customer-email-fieldset" class="fieldset" data-bind="blockLoader: isLoading"> <div class="field required"> - <label class="label" for="checkout-customer-email"> + <label class="label" for="customer-email"> <span data-bind="i18n: 'Email Address'"></span> </label> <div class="control _with-tooltip"> @@ -26,7 +26,7 @@ mageInit: {'mage/trim-input':{}}" name="username" data-validate="{required:true, 'validate-email':true}" - id="checkout-customer-email" /> + id="customer-email" /> <!-- ko template: 'ui/form/element/helper/tooltip' --><!-- /ko --> <span class="note" data-bind="fadeVisible: isPasswordVisible() == false"><!-- ko i18n: 'You can create an account after checkout.'--><!-- /ko --></span> </div> diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php index 807bdcb015ad6..b21ea9fd7ef7b 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php @@ -7,8 +7,14 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg; use Magento\Backend\App\Action; +use Magento\Cms\Model\Template\Filter; +use Magento\Cms\Model\Wysiwyg\Config; +use Magento\Framework\App\Action\HttpGetActionInterface; -class Directive extends \Magento\Backend\App\Action +/** + * Process template text for wysiwyg editor. + */ +class Directive extends Action implements HttpGetActionInterface { /** @@ -52,19 +58,21 @@ public function execute() { $directive = $this->getRequest()->getParam('___directive'); $directive = $this->urlDecoder->decode($directive); - $imagePath = $this->_objectManager->create(\Magento\Cms\Model\Template\Filter::class)->filter($directive); - /** @var \Magento\Framework\Image\Adapter\AdapterInterface $image */ - $image = $this->_objectManager->get(\Magento\Framework\Image\AdapterFactory::class)->create(); - /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ - $resultRaw = $this->resultRawFactory->create(); try { + /** @var Filter $filter */ + $filter = $this->_objectManager->create(Filter::class); + $imagePath = $filter->filter($directive); + /** @var \Magento\Framework\Image\Adapter\AdapterInterface $image */ + $image = $this->_objectManager->get(\Magento\Framework\Image\AdapterFactory::class)->create(); + /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ + $resultRaw = $this->resultRawFactory->create(); $image->open($imagePath); $resultRaw->setHeader('Content-Type', $image->getMimeType()); $resultRaw->setContents($image->getImage()); } catch (\Exception $e) { - $imagePath = $this->_objectManager->get( - \Magento\Cms\Model\Wysiwyg\Config::class - )->getSkinImagePlaceholderPath(); + /** @var Config $config */ + $config = $this->_objectManager->get(Config::class); + $imagePath = $config->getSkinImagePlaceholderPath(); $image->open($imagePath); $resultRaw->setHeader('Content-Type', $image->getMimeType()); $resultRaw->setContents($image->getImage()); diff --git a/app/code/Magento/Cms/Model/Page/DataProvider.php b/app/code/Magento/Cms/Model/Page/DataProvider.php index abbe9254a68d4..64abaffd04e66 100644 --- a/app/code/Magento/Cms/Model/Page/DataProvider.php +++ b/app/code/Magento/Cms/Model/Page/DataProvider.php @@ -6,8 +6,10 @@ namespace Magento\Cms\Model\Page; use Magento\Cms\Model\ResourceModel\Page\CollectionFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Ui\DataProvider\Modifier\PoolInterface; +use Magento\Framework\AuthorizationInterface; /** * Class DataProvider @@ -29,6 +31,11 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider */ protected $loadedData; + /** + * @var AuthorizationInterface + */ + private $auth; + /** * @param string $name * @param string $primaryFieldName @@ -38,6 +45,7 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider * @param array $meta * @param array $data * @param PoolInterface|null $pool + * @param AuthorizationInterface|null $auth */ public function __construct( $name, @@ -47,11 +55,13 @@ public function __construct( DataPersistorInterface $dataPersistor, array $meta = [], array $data = [], - PoolInterface $pool = null + PoolInterface $pool = null, + ?AuthorizationInterface $auth = null ) { $this->collection = $pageCollectionFactory->create(); $this->dataPersistor = $dataPersistor; parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data, $pool); + $this->auth = $auth ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); $this->meta = $this->prepareMeta($this->meta); } @@ -92,4 +102,38 @@ public function getData() return $this->loadedData; } + + /** + * @inheritDoc + */ + public function getMeta() + { + $meta = parent::getMeta(); + + if (!$this->auth->isAllowed('Magento_Cms::save_design')) { + $designMeta = [ + 'design' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'disabled' => true + ] + ] + ] + ], + 'custom_design_update' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'disabled' => true + ] + ] + ] + ] + ]; + $meta = array_merge_recursive($meta, $designMeta); + } + + return $meta; + } } diff --git a/app/code/Magento/Cms/Model/PageRepository.php b/app/code/Magento/Cms/Model/PageRepository.php index 5578ae49f586d..e6777659d7d88 100644 --- a/app/code/Magento/Cms/Model/PageRepository.php +++ b/app/code/Magento/Cms/Model/PageRepository.php @@ -10,6 +10,7 @@ use Magento\Cms\Api\PageRepositoryInterface; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; @@ -17,6 +18,8 @@ use Magento\Cms\Model\ResourceModel\Page as ResourcePage; use Magento\Cms\Model\ResourceModel\Page\CollectionFactory as PageCollectionFactory; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Authorization\Model\UserContextInterface; /** * Class PageRepository @@ -69,6 +72,16 @@ class PageRepository implements PageRepositoryInterface */ private $collectionProcessor; + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * @param ResourcePage $resource * @param PageFactory $pageFactory @@ -79,6 +92,7 @@ class PageRepository implements PageRepositoryInterface * @param DataObjectProcessor $dataObjectProcessor * @param StoreManagerInterface $storeManager * @param CollectionProcessorInterface $collectionProcessor + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ResourcePage $resource, @@ -102,10 +116,38 @@ public function __construct( $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); } + /** + * Get user context. + * + * @return UserContextInterface + */ + private function getUserContext(): UserContextInterface + { + if (!$this->userContext) { + $this->userContext = ObjectManager::getInstance()->get(UserContextInterface::class); + } + + return $this->userContext; + } + + /** + * Get authorization service. + * + * @return AuthorizationInterface + */ + private function getAuthorization(): AuthorizationInterface + { + if (!$this->authorization) { + $this->authorization = ObjectManager::getInstance()->get(AuthorizationInterface::class); + } + + return $this->authorization; + } + /** * Save Page data * - * @param \Magento\Cms\Api\Data\PageInterface $page + * @param \Magento\Cms\Api\Data\PageInterface|Page $page * @return Page * @throws CouldNotSaveException */ @@ -116,6 +158,32 @@ public function save(\Magento\Cms\Api\Data\PageInterface $page) $page->setStoreId($storeId); } try { + //Validate changing of design. + $userType = $this->getUserContext()->getUserType(); + if (( + $userType === UserContextInterface::USER_TYPE_ADMIN + || $userType === UserContextInterface::USER_TYPE_INTEGRATION + ) + && !$this->getAuthorization()->isAllowed('Magento_Cms::save_design') + ) { + if (!$page->getId()) { + $page->setLayoutUpdateXml(null); + $page->setPageLayout(null); + $page->setCustomTheme(null); + $page->setCustomLayoutUpdateXml(null); + $page->setCustomThemeTo(null); + $page->setCustomThemeFrom(null); + } else { + $savedPage = $this->getById($page->getId()); + $page->setLayoutUpdateXml($savedPage->getLayoutUpdateXml()); + $page->setPageLayout($savedPage->getPageLayout()); + $page->setCustomTheme($savedPage->getCustomTheme()); + $page->setCustomLayoutUpdateXml($savedPage->getCustomLayoutUpdateXml()); + $page->setCustomThemeTo($savedPage->getCustomThemeTo()); + $page->setCustomThemeFrom($savedPage->getCustomThemeFrom()); + } + } + $this->resource->save($page); } catch (\Exception $exception) { throw new CouldNotSaveException( @@ -178,10 +246,9 @@ public function delete(\Magento\Cms\Api\Data\PageInterface $page) try { $this->resource->delete($page); } catch (\Exception $exception) { - throw new CouldNotDeleteException(__( - 'Could not delete the page: %1', - $exception->getMessage() - )); + throw new CouldNotDeleteException( + __('Could not delete the page: %1', $exception->getMessage()) + ); } return true; } diff --git a/app/code/Magento/Cms/Model/Template/Filter.php b/app/code/Magento/Cms/Model/Template/Filter.php index f93972bade2af..7e71a06de1f31 100644 --- a/app/code/Magento/Cms/Model/Template/Filter.php +++ b/app/code/Magento/Cms/Model/Template/Filter.php @@ -5,6 +5,8 @@ */ namespace Magento\Cms\Model\Template; +use Magento\Framework\Exception\LocalizedException; + /** * Cms Template Filter Model */ @@ -37,7 +39,12 @@ public function setUseSessionInUrl($flag) */ public function mediaDirective($construction) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $params = $this->getParameters(html_entity_decode($construction[2], ENT_QUOTES)); + if (preg_match('/\.\.(\\\|\/)/', $params['url'])) { + throw new \InvalidArgumentException('Image path must be absolute'); + } + return $this->_storeManager->getStore()->getBaseMediaDir() . '/' . $params['url']; } } diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index dfbbce99b6515..6cfa43eb36e2c 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -328,6 +328,7 @@ public function getFilesCollection($path, $type = null) $item->setName($item->getBasename()); $item->setShortName($this->_cmsWysiwygImages->getShortFilename($item->getBasename())); $item->setUrl($this->_cmsWysiwygImages->getCurrentUrl() . $item->getBasename()); + // phpcs:ignore Magento2.Functions.DiscouragedFunction $item->setSize(filesize($item->getFilename())); $item->setMimeType(\mime_content_type($item->getFilename())); @@ -338,6 +339,7 @@ public function getFilesCollection($path, $type = null) $thumbUrl = $this->_backendUrl->getUrl('cms/*/thumbnail', ['file' => $item->getId()]); } + // phpcs:ignore Generic.PHP.NoSilencedErrors $size = @getimagesize($item->getFilename()); if (is_array($size)) { @@ -413,6 +415,7 @@ public function createDirectory($name, $path) 'id' => $this->_cmsWysiwygImages->convertPathToId($newPath), ]; return $result; + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\FileSystemException $e) { throw new \Magento\Framework\Exception\LocalizedException(__('We cannot create a new directory.')); } @@ -421,7 +424,7 @@ public function createDirectory($name, $path) /** * Recursively delete directory from storage * - * @param string $path Target dir + * @param string $path Absolute path to target directory * @return void * @throws \Magento\Framework\Exception\LocalizedException */ @@ -430,12 +433,20 @@ public function deleteDirectory($path) if ($this->_coreFileStorageDb->checkDbUsage()) { $this->_directoryDatabaseFactory->create()->deleteDirectory($path); } + if (!$this->isPathAllowed($path, $this->getConditionsForExcludeDirs())) { + throw new \Magento\Framework\Exception\LocalizedException( + __('We cannot delete directory %1.', $this->_getRelativePathToRoot($path)) + ); + } try { $this->_deleteByPath($path); $path = $this->getThumbnailRoot() . $this->_getRelativePathToRoot($path); $this->_deleteByPath($path); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\FileSystemException $e) { - throw new \Magento\Framework\Exception\LocalizedException(__('We cannot delete directory %1.', $path)); + throw new \Magento\Framework\Exception\LocalizedException( + __('We cannot delete directory %1.', $this->_getRelativePathToRoot($path)) + ); } } @@ -482,13 +493,18 @@ public function deleteFile($target) /** * Upload and resize new file * - * @param string $targetPath Target directory + * @param string $targetPath Absolute path to target directory * @param string $type Type of storage, e.g. image, media etc. * @return array File info Array * @throws \Magento\Framework\Exception\LocalizedException */ public function uploadFile($targetPath, $type = null) { + if (!$this->isPathAllowed($targetPath, $this->getConditionsForExcludeDirs())) { + throw new \Magento\Framework\Exception\LocalizedException( + __('We can\'t upload the file to current folder right now. Please try another folder.') + ); + } /** @var \Magento\MediaStorage\Model\File\Uploader $uploader */ $uploader = $this->_uploaderFactory->create(['fileId' => 'image']); $allowed = $this->getAllowedExtensions($type); @@ -589,6 +605,7 @@ public function resizeFile($source, $keepRatio = true) $image->open($source); $image->keepAspectRatio($keepRatio); $image->resize($this->_resizeParameters['width'], $this->_resizeParameters['height']); + // phpcs:ignore Magento2.Functions.DiscouragedFunction $dest = $targetDir . '/' . pathinfo($source, PATHINFO_BASENAME); $image->save($dest); if ($this->_directory->isFile($this->_directory->getRelativePath($dest))) { @@ -624,6 +641,7 @@ public function getThumbsPath($filePath = false) $thumbnailDir = $this->getThumbnailRoot(); if ($filePath && strpos($filePath, $mediaRootDir) === 0) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $thumbnailDir .= dirname(substr($filePath, strlen($mediaRootDir))); } @@ -674,6 +692,7 @@ public function isImage($filename) if (!$this->hasData('_image_extensions')) { $this->setData('_image_extensions', $this->getAllowedExtensions('image')); } + // phpcs:ignore Magento2.Functions.DiscouragedFunction $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); return in_array($ext, $this->_getData('_image_extensions')); } @@ -784,4 +803,29 @@ private function getExtensionsList($type = null): array return $allowed; } + + /** + * Check if path is not in excluded dirs. + * + * @param string $path Absolute path + * @param array $conditions Exclude conditions + * @return bool + */ + private function isPathAllowed($path, array $conditions): bool + { + $isAllowed = true; + $regExp = $conditions['reg_exp'] ? '~' . implode('|', array_keys($conditions['reg_exp'])) . '~i' : null; + $storageRoot = $this->_cmsWysiwygImages->getStorageRoot(); + $storageRootLength = strlen($storageRoot); + + $mediaSubPathname = substr($path, $storageRootLength); + $rootChildParts = explode('/', '/' . ltrim($mediaSubPathname, '/')); + + if (array_key_exists($rootChildParts[1], $conditions['plain']) + || ($regExp && preg_match($regExp, $path))) { + $isAllowed = false; + } + + return $isAllowed; + } } diff --git a/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php index 09476f291fb06..b6b802a8a4e6d 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php @@ -46,6 +46,8 @@ protected function setUp() } /** + * Test processing media directives. + * * @covers \Magento\Cms\Model\Template\Filter::mediaDirective */ public function testMediaDirective() @@ -63,6 +65,11 @@ public function testMediaDirective() $this->assertEquals($expectedResult, $this->filter->mediaDirective($construction)); } + /** + * Test the directive when HTML quotes used. + * + * @covers \Magento\Cms\Model\Template\Filter::mediaDirective + */ public function testMediaDirectiveWithEncodedQuotes() { $baseMediaDir = 'pub/media'; @@ -78,4 +85,24 @@ public function testMediaDirectiveWithEncodedQuotes() ->willReturn($baseMediaDir); $this->assertEquals($expectedResult, $this->filter->mediaDirective($construction)); } + + /** + * Test using media directive with relative path to image. + * + * @covers \Magento\Cms\Model\Template\Filter::mediaDirective + * @expectedException \InvalidArgumentException + */ + public function testMediaDirectiveRelativePath() + { + $baseMediaDir = 'pub/media'; + $construction = [ + '{{media url="wysiwyg/images/../image.jpg"}}', + 'media', + ' url="wysiwyg/images/../image.jpg"' + ]; + $this->storeMock->expects($this->any()) + ->method('getBaseMediaDir') + ->willReturn($baseMediaDir); + $this->filter->mediaDirective($construction); + } } diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index 7bec1e3601461..6cf38324b3917 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -18,7 +18,7 @@ class StorageTest extends \PHPUnit\Framework\TestCase /** * Directory paths samples */ - const STORAGE_ROOT_DIR = '/storage/root/dir'; + const STORAGE_ROOT_DIR = '/storage/root/dir/'; const INVALID_DIRECTORY_OVER_ROOT = '/storage/some/another/dir'; @@ -437,10 +437,11 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e public function testUploadFile() { - $targetPath = '/target/path'; + $path = 'target/path'; + $targetPath = self::STORAGE_ROOT_DIR . $path; $fileName = 'image.gif'; $realPath = $targetPath . '/' . $fileName; - $thumbnailTargetPath = self::STORAGE_ROOT_DIR . '/.thumbs'; + $thumbnailTargetPath = self::STORAGE_ROOT_DIR . '/.thumbs' . $path; $thumbnailDestination = $thumbnailTargetPath . '/' . $fileName; $type = 'image'; $result = [ 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 3095abef7bbe3..8741e37016b64 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php @@ -14,7 +14,7 @@ use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * BlockActionsTest contains unit tests for \Magento\Cms\Ui\Component\Listing\Column\BlockActions class + * BlockActionsTest contains unit tests for \Magento\Cms\Ui\Component\Listing\Column\BlockActions class. */ class BlockActionsTest extends \PHPUnit\Framework\TestCase { @@ -33,6 +33,9 @@ class BlockActionsTest extends \PHPUnit\Framework\TestCase */ private $urlBuilder; + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); @@ -42,7 +45,7 @@ protected function setUp() $processor = $this->getMockBuilder(Processor::class) ->disableOriginalConstructor() ->getMock(); - $context->expects(static::never()) + $context->expects($this->never()) ->method('getProcessor') ->willReturn($processor); @@ -50,19 +53,22 @@ protected function setUp() $this->escaper = $this->getMockBuilder(Escaper::class) ->disableOriginalConstructor() - ->setMethods(['escapeHtml']) + ->setMethods(['escapeHtmlAttr']) ->getMock(); - $this->blockActions = $objectManager->getObject(BlockActions::class, [ - 'context' => $context, - 'urlBuilder' => $this->urlBuilder - ]); + $this->blockActions = $objectManager->getObject( + BlockActions::class, + ['context' => $context, 'urlBuilder' => $this->urlBuilder] + ); $objectManager->setBackwardCompatibleProperty($this->blockActions, 'escaper', $this->escaper); } /** + * Unit test for prepareDataSource method. + * * @covers \Magento\Cms\Ui\Component\Listing\Column\BlockActions::prepareDataSource + * @return void */ public function testPrepareDataSource() { @@ -73,10 +79,10 @@ public function testPrepareDataSource() 'items' => [ [ 'block_id' => $blockId, - 'title' => $title - ] - ] - ] + 'title' => $title, + ], + ], + ], ]; $name = 'item_name'; $expectedItems = [ @@ -93,34 +99,34 @@ public function testPrepareDataSource() 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) + 'message' => __('Are you sure you want to delete a %1 record?', $title), ], - 'post' => true - ] + 'post' => true, + ], ], - ] + ], ]; - $this->escaper->expects(static::once()) - ->method('escapeHtml') + $this->escaper->expects($this->once()) + ->method('escapeHtmlAttr') ->with($title) ->willReturn($title); - $this->urlBuilder->expects(static::exactly(2)) + $this->urlBuilder->expects($this->exactly(2)) ->method('getUrl') ->willReturnMap( [ [ BlockActions::URL_PATH_EDIT, [ - 'block_id' => $blockId + 'block_id' => $blockId, ], 'test/url/edit', ], [ BlockActions::URL_PATH_DELETE, [ - 'block_id' => $blockId + 'block_id' => $blockId, ], 'test/url/delete', ], @@ -130,6 +136,6 @@ public function testPrepareDataSource() $this->blockActions->setData('name', $name); $actual = $this->blockActions->prepareDataSource($items); - static::assertEquals($expectedItems, $actual['data']['items']); + $this->assertEquals($expectedItems, $actual['data']['items']); } } diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php index 9b3165a2c5517..32bbeed0788a3 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php @@ -8,6 +8,9 @@ use Magento\Cms\Ui\Component\Listing\Column\PageActions; use Magento\Framework\Escaper; +/** + * Test for Magento\Cms\Ui\Component\Listing\Column\PageActions class. + */ class PageActionsTest extends \PHPUnit\Framework\TestCase { public function testPrepareItemsByPageId() @@ -68,12 +71,13 @@ public function testPrepareItemsByPageId() 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) + 'message' => __('Are you sure you want to delete a %1 record?', $title), + '__disableTmpl' => true, ], - 'post' => true - ] + 'post' => true, + ], ], - ] + ], ]; $escaper->expects(static::once()) 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 f68ef35e534f3..6e9eef47281c0 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php @@ -13,7 +13,7 @@ use Magento\Framework\Escaper; /** - * Class BlockActions + * Class to build edit and delete link for each item. */ class BlockActions extends Column { @@ -35,8 +35,6 @@ class BlockActions extends Column private $escaper; /** - * Constructor - * * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param UrlInterface $urlBuilder @@ -62,31 +60,31 @@ public function prepareDataSource(array $dataSource) if (isset($dataSource['data']['items'])) { foreach ($dataSource['data']['items'] as & $item) { if (isset($item['block_id'])) { - $title = $this->getEscaper()->escapeHtml($item['title']); + $title = $this->getEscaper()->escapeHtmlAttr($item['title']); $item[$this->getData('name')] = [ 'edit' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_EDIT, [ - 'block_id' => $item['block_id'] + 'block_id' => $item['block_id'], ] ), - 'label' => __('Edit') + 'label' => __('Edit'), ], 'delete' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_DELETE, [ - 'block_id' => $item['block_id'] + 'block_id' => $item['block_id'], ] ), 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) + 'message' => __('Are you sure you want to delete a %1 record?', $title), ], - 'post' => true - ] + 'post' => true, + ], ]; } } 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 9c57aa050b01b..5cefa212d1655 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php @@ -94,9 +94,10 @@ public function prepareDataSource(array $dataSource) 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) + 'message' => __('Are you sure you want to delete a %1 record?', $title), + '__disableTmpl' => true, ], - 'post' => true + 'post' => true, ]; } if (isset($item['identifier'])) { diff --git a/app/code/Magento/Cms/composer.json b/app/code/Magento/Cms/composer.json index f051271c05051..b80ab60a7ec64 100644 --- a/app/code/Magento/Cms/composer.json +++ b/app/code/Magento/Cms/composer.json @@ -15,7 +15,8 @@ "magento/module-theme": "*", "magento/module-ui": "*", "magento/module-variable": "*", - "magento/module-widget": "*" + "magento/module-widget": "*", + "magento/module-authorization": "*" }, "suggest": { "magento/module-cms-sample-data": "*" diff --git a/app/code/Magento/Cms/etc/acl.xml b/app/code/Magento/Cms/etc/acl.xml index b13b58b101f90..3df31923d1eb6 100644 --- a/app/code/Magento/Cms/etc/acl.xml +++ b/app/code/Magento/Cms/etc/acl.xml @@ -12,7 +12,9 @@ <resource id="Magento_Backend::content"> <resource id="Magento_Backend::content_elements"> <resource id="Magento_Cms::page" title="Pages" translate="title" sortOrder="10"> - <resource id="Magento_Cms::save" title="Save Page" translate="title" sortOrder="10" /> + <resource id="Magento_Cms::save" title="Save Page" translate="title" sortOrder="10"> + <resource id="Magento_Cms::save_design" title="Edit Page Design" translate="title" /> + </resource> <resource id="Magento_Cms::page_delete" title="Delete Page" translate="title" sortOrder="20" /> </resource> <resource id="Magento_Cms::block" title="Blocks" translate="title" sortOrder="30" /> diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 025d63115f4ce..e41f500915916 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -47,7 +47,6 @@ </item> <item name="media_allowed" xsi:type="array"> <item name="flv" xsi:type="string">video/x-flv</item> - <item name="swf" xsi:type="string">application/x-shockwave-flash</item> <item name="avi" xsi:type="string">video/x-msvideo</item> <item name="mov" xsi:type="string">video/x-sgi-movie</item> <item name="rm" xsi:type="string">application/vnd.rn-realmedia</item> diff --git a/app/code/Magento/Cms/i18n/en_US.csv b/app/code/Magento/Cms/i18n/en_US.csv index b34793c35d659..2947c567d75ff 100644 --- a/app/code/Magento/Cms/i18n/en_US.csv +++ b/app/code/Magento/Cms/i18n/en_US.csv @@ -155,3 +155,4 @@ Enable,Enable "Custom design to","Custom design to" "Custom Theme","Custom Theme" "Custom Layout","Custom Layout" +"Edit Page Design","Edit Page Design" diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php index a40d23968c3c6..8090dbfbd39b1 100644 --- a/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php @@ -15,6 +15,9 @@ */ class Identity implements IdentityInterface { + /** @var string */ + private $cacheTag = \Magento\Cms\Model\Block::CACHE_TAG; + /** * Get block identities from resolved data * @@ -27,11 +30,15 @@ public function getIdentities(array $resolvedData): array $items = $resolvedData['items'] ?? []; foreach ($items as $item) { if (is_array($item) && !empty($item[BlockInterface::BLOCK_ID])) { - $ids[] = $item[BlockInterface::BLOCK_ID]; - $ids[] = $item[BlockInterface::IDENTIFIER]; + $ids[] = sprintf('%s_%s', $this->cacheTag, $item[BlockInterface::BLOCK_ID]); + $ids[] = sprintf('%s_%s', $this->cacheTag, $item[BlockInterface::IDENTIFIER]); } } + if (!empty($ids)) { + array_unshift($ids, $this->cacheTag); + } + return $ids; } } diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php index abc306451e309..cbfe9a7f9e1e9 100644 --- a/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php @@ -15,6 +15,9 @@ */ class Identity implements IdentityInterface { + /** @var string */ + private $cacheTag = \Magento\Cms\Model\Page::CACHE_TAG; + /** * Get page ID from resolved data * @@ -23,6 +26,7 @@ class Identity implements IdentityInterface */ public function getIdentities(array $resolvedData): array { - return empty($resolvedData[PageInterface::PAGE_ID]) ? [] : [$resolvedData[PageInterface::PAGE_ID]]; + return empty($resolvedData[PageInterface::PAGE_ID]) ? + [] : [$this->cacheTag, sprintf('%s_%s', $this->cacheTag, $resolvedData[PageInterface::PAGE_ID])]; } } diff --git a/app/code/Magento/CmsGraphQl/etc/schema.graphqls b/app/code/Magento/CmsGraphQl/etc/schema.graphqls index 3558d853aa4df..2453cb61b9a6d 100644 --- a/app/code/Magento/CmsGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CmsGraphQl/etc/schema.graphqls @@ -14,10 +14,10 @@ type Query { cmsPage ( id: Int @doc(description: "Id of the CMS page") @deprecated(reason: "The `id` is deprecated. Use `identifier` instead.") @doc(description: "The CMS page query returns information about a CMS page") identifier: String @doc(description: "Identifier of the CMS page") - ): CmsPage @resolver(class: "Magento\\CmsGraphQl\\Model\\Resolver\\Page") @doc(description: "The CMS page query returns information about a CMS page") @cache(cacheTag: "cms_p", cacheIdentity: "Magento\\CmsGraphQl\\Model\\Resolver\\Page\\Identity") + ): CmsPage @resolver(class: "Magento\\CmsGraphQl\\Model\\Resolver\\Page") @doc(description: "The CMS page query returns information about a CMS page") @cache(cacheIdentity: "Magento\\CmsGraphQl\\Model\\Resolver\\Page\\Identity") cmsBlocks ( identifiers: [String] @doc(description: "Identifiers of the CMS blocks") - ): CmsBlocks @resolver(class: "Magento\\CmsGraphQl\\Model\\Resolver\\Blocks") @doc(description: "The CMS block query returns information about CMS blocks") @cache(cacheTag: "cms_b", cacheIdentity: "Magento\\CmsGraphQl\\Model\\Resolver\\Block\\Identity") + ): CmsBlocks @resolver(class: "Magento\\CmsGraphQl\\Model\\Resolver\\Blocks") @doc(description: "The CMS block query returns information about CMS blocks") @cache(cacheIdentity: "Magento\\CmsGraphQl\\Model\\Resolver\\Block\\Identity") } type CmsPage @doc(description: "CMS page defines all CMS page information") { diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CmsUrlResolverIdentity.php b/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CmsUrlResolverIdentity.php new file mode 100644 index 0000000000000..7025217d1186b --- /dev/null +++ b/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CmsUrlResolverIdentity.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsUrlRewriteGraphQl\Model\Resolver\UrlRewrite; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Get ids from cms page url rewrite + */ +class CmsUrlResolverIdentity implements IdentityInterface +{ + /** @var string */ + private $cacheTag = \Magento\Cms\Model\Page::CACHE_TAG; + + /** + * Get identities cache ID from a url rewrite entities + * + * @param array $resolvedData + * @return string[] + */ + public function getIdentities(array $resolvedData): array + { + $ids = []; + if (isset($resolvedData['id'])) { + $selectedCacheTag = $this->cacheTag; + $ids = [$selectedCacheTag, sprintf('%s_%s', $selectedCacheTag, $resolvedData['id'])]; + } + return $ids; + } +} diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/composer.json b/app/code/Magento/CmsUrlRewriteGraphQl/composer.json index c57e4cdc92a83..03436d39190ac 100644 --- a/app/code/Magento/CmsUrlRewriteGraphQl/composer.json +++ b/app/code/Magento/CmsUrlRewriteGraphQl/composer.json @@ -5,9 +5,9 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-url-rewrite-graph-ql": "*", + "magento/module-cms": "*", "magento/module-store": "*", - "magento/module-cms": "*" + "magento/module-url-rewrite-graph-ql": "*" }, "suggest": { "magento/module-cms-url-rewrite": "*", diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/etc/di.xml b/app/code/Magento/CmsUrlRewriteGraphQl/etc/di.xml index d384c898acb62..ae8475cc113d2 100644 --- a/app/code/Magento/CmsUrlRewriteGraphQl/etc/di.xml +++ b/app/code/Magento/CmsUrlRewriteGraphQl/etc/di.xml @@ -13,4 +13,11 @@ </argument> </arguments> </type> + <type name="Magento\UrlRewriteGraphQl\Model\Resolver\UrlRewrite\UrlResolverIdentity"> + <arguments> + <argument name="urlResolverIdentities" xsi:type="array"> + <item name="cms_page" xsi:type="object">Magento\CmsUrlRewriteGraphQl\Model\Resolver\UrlRewrite\CmsUrlResolverIdentity</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php b/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php index 36e4603cba577..274ee3b8d2a6e 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php @@ -10,8 +10,11 @@ /** * System Configuration Abstract Controller + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 + * + * @SuppressWarnings(PHPMD.AllPurposeAction) */ abstract class AbstractConfig extends \Magento\Backend\App\AbstractAction { 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 2d4b20033806e..91bcb632cf73b 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Config\Controller\Adminhtml\System\Config; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; @@ -55,6 +56,68 @@ public function __construct( $this->string = $string; } + /** + * @inheritdoc + */ + protected function _isAllowed() + { + return parent::_isAllowed() && $this->isSectionAllowed(); + } + + /** + * Checks if user has access to section. + * + * @return bool + */ + private function isSectionAllowed(): bool + { + $sectionId = $this->_request->getParam('section'); + $isAllowed = $this->_configStructure->getElement($sectionId)->isAllowed(); + if (!$isAllowed) { + $groups = $this->getRequest()->getPost('groups'); + $fieldPath = $this->getFirstFieldPath($groups, $sectionId); + + $fieldPaths = $this->_configStructure->getFieldPaths(); + $fieldPath = $fieldPaths[$fieldPath][0] ?? $sectionId; + $explodedConfigPath = explode('/', $fieldPath); + $configSectionId = $explodedConfigPath[0] ?? $sectionId; + + $isAllowed = $this->_configStructure->getElement($configSectionId)->isAllowed(); + } + + return $isAllowed; + } + + /** + * Return field path as string. + * + * @param array $elements + * @param string $fieldPath + * @return string + */ + private function getFirstFieldPath(array $elements, string $fieldPath): string + { + $groupData = []; + foreach ($elements as $elementName => $element) { + if (!empty($element)) { + $fieldPath .= '/' . $elementName; + + if (!empty($element['fields'])) { + $groupData = $element['fields']; + } elseif (!empty($element['groups'])) { + $groupData = $element['groups']; + } + + if (!empty($groupData)) { + $fieldPath = $this->getFirstFieldPath($groupData, $fieldPath); + } + break; + } + } + + return $fieldPath; + } + /** * Get groups for save * @@ -150,20 +213,21 @@ public function execute() $section = $this->getRequest()->getParam('section'); $website = $this->getRequest()->getParam('website'); $store = $this->getRequest()->getParam('store'); - $configData = [ 'section' => $section, 'website' => $website, 'store' => $store, 'groups' => $this->_getGroupsForSave(), ]; - /** @var \Magento\Config\Model\Config $configModel */ + $configData = $this->filterNodes($configData); + + /** @var \Magento\Config\Model\Config $configModel */ $configModel = $this->_configFactory->create(['data' => $configData]); $configModel->save(); - $this->_eventManager->dispatch('admin_system_config_save', [ - 'configData' => $configData, - 'request' => $this->getRequest() - ]); + $this->_eventManager->dispatch( + 'admin_system_config_save', + ['configData' => $configData, 'request' => $this->getRequest()] + ); $this->messageManager->addSuccess(__('You saved the configuration.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { $messages = explode("\n", $e->getMessage()); @@ -188,4 +252,86 @@ public function execute() ] ); } + + /** + * Filter paths that are not defined. + * + * @param string $prefix Path prefix + * @param array $groups Groups data. + * @param string[] $systemXmlConfig Defined paths. + * @return array Filtered groups. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function filterPaths(string $prefix, array $groups, array $systemXmlConfig): array + { + $flippedXmlConfig = array_flip($systemXmlConfig); + $filtered = []; + foreach ($groups as $groupName => $childPaths) { + //When group accepts arbitrary fields and clones them we allow it + $group = $this->_configStructure->getElement($prefix .'/' .$groupName); + if (array_key_exists('clone_fields', $group->getData()) && $group->getData()['clone_fields']) { + $filtered[$groupName] = $childPaths; + continue; + } + + $filtered[$groupName] = ['fields' => [], 'groups' => []]; + //Processing fields + if (array_key_exists('fields', $childPaths)) { + foreach ($childPaths['fields'] as $field => $fieldData) { + //Constructing config path for the $field + $path = $prefix .'/' .$groupName .'/' .$field; + $element = $this->_configStructure->getElement($path); + if ($element + && ($elementData = $element->getData()) + && array_key_exists('config_path', $elementData) + ) { + $path = $elementData['config_path']; + } + //Checking whether it exists in system.xml + if (array_key_exists($path, $flippedXmlConfig)) { + $filtered[$groupName]['fields'][$field] = $fieldData; + } + } + } + //Recursively filtering this group's groups. + if (array_key_exists('groups', $childPaths) && $childPaths['groups']) { + $filteredGroups = $this->filterPaths( + $prefix .'/' .$groupName, + $childPaths['groups'], + $systemXmlConfig + ); + if ($filteredGroups) { + $filtered[$groupName]['groups'] = $filteredGroups; + } + } + + $filtered[$groupName] = array_filter($filtered[$groupName]); + } + + return array_filter($filtered); + } + + /** + * Filters nodes by checking whether they exist in system.xml. + * + * @param array $configData + * @return array + */ + private function filterNodes(array $configData): array + { + if (!empty($configData['groups'])) { + $systemXmlPathsFromKeys = array_keys($this->_configStructure->getFieldPaths()); + $systemXmlPathsFromValues = array_reduce( + array_values($this->_configStructure->getFieldPaths()), + 'array_merge', + [] + ); + //Full list of paths defined in system.xml + $systemXmlConfig = array_merge($systemXmlPathsFromKeys, $systemXmlPathsFromValues); + + $configData['groups'] = $this->filterPaths($configData['section'], $configData['groups'], $systemXmlConfig); + } + + return $configData; + } } diff --git a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php index ea3b1d4c74a5f..62d6531978d8a 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php +++ b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php @@ -1,7 +1,5 @@ <?php /** - * Encrypted config field backend model - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,7 +7,7 @@ namespace Magento\Config\Model\Config\Backend; /** - * Backend model for encrypted values. + * Encrypted config field backend model. * * @api * @since 100.0.2 @@ -50,14 +48,9 @@ public function __construct( * Magic method called during class serialization * * @return string[] - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $properties = parent::__sleep(); return array_diff($properties, ['_encryptor']); } @@ -66,14 +59,9 @@ public function __sleep() * Magic method called during class un-serialization * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $this->_encryptor = \Magento\Framework\App\ObjectManager::getInstance()->get( \Magento\Framework\Encryption\EncryptorInterface::class diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php deleted file mode 100644 index 069a1c20b2966..0000000000000 --- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php +++ /dev/null @@ -1,292 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Config\Test\Unit\Controller\Adminhtml\System\Config; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SaveTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Config\Controller\Adminhtml\System\Config\Save - */ - protected $_controller; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_requestMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_configFactoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_eventManagerMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_authMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_sectionMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_cacheMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_responseMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_sectionCheckerMock; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $resultRedirect; - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp() - { - $this->_requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); - $this->_responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); - - $configStructureMock = $this->createMock(\Magento\Config\Model\Config\Structure::class); - $this->_configFactoryMock = $this->createMock(\Magento\Config\Model\Config\Factory::class); - $this->_eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - - $helperMock = $this->createMock(\Magento\Backend\Helper\Data::class); - - $this->messageManagerMock = $this->createPartialMock( - \Magento\Framework\Message\Manager::class, - ['addSuccess', 'addException'] - ); - - $this->_authMock = $this->createPartialMock(\Magento\Backend\Model\Auth::class, ['getUser']); - - $this->_sectionMock = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Section::class); - - $this->_cacheMock = $this->createMock(\Magento\Framework\App\Cache\Type\Layout::class); - - $configStructureMock->expects($this->any())->method('getElement')->willReturn($this->_sectionMock); - $configStructureMock->expects($this->any())->method('getSectionList')->willReturn( - [ - 'some_key_0' => '0', - 'some_key_1' => '1' - ] - ); - - $helperMock->expects($this->any())->method('getUrl')->willReturnArgument(0); - - $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resultRedirect->expects($this->atLeastOnce()) - ->method('setPath') - ->with('adminhtml/system_config/edit') - ->willReturnSelf(); - $resultRedirectFactory = $this->getMockBuilder(\Magento\Backend\Model\View\Result\RedirectFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $resultRedirectFactory->expects($this->atLeastOnce()) - ->method('create') - ->willReturn($this->resultRedirect); - - $arguments = [ - 'request' => $this->_requestMock, - 'response' => $this->_responseMock, - 'helper' => $helperMock, - 'eventManager' => $this->_eventManagerMock, - 'auth' => $this->_authMock, - 'messageManager' => $this->messageManagerMock, - 'resultRedirectFactory' => $resultRedirectFactory - ]; - - $this->_sectionCheckerMock = $this->createMock( - \Magento\Config\Controller\Adminhtml\System\ConfigSectionChecker::class - ); - - $context = $helper->getObject(\Magento\Backend\App\Action\Context::class, $arguments); - $this->_controller = $this->getMockBuilder(\Magento\Config\Controller\Adminhtml\System\Config\Save::class) - ->setMethods(['deniedAction']) - ->setConstructorArgs( - [ - $context, - $configStructureMock, - $this->_sectionCheckerMock, - $this->_configFactoryMock, - $this->_cacheMock, - new \Magento\Framework\Stdlib\StringUtils(), - ] - ) - ->getMock(); - } - - public function testIndexActionWithAllowedSection() - { - $this->_sectionCheckerMock->expects($this->any())->method('isSectionAllowed')->will($this->returnValue(true)); - $this->messageManagerMock->expects($this->once())->method('addSuccess')->with('You saved the configuration.'); - - $groups = ['some_key' => 'some_value']; - $requestParamMap = [ - ['section', null, 'test_section'], - ['website', null, 'test_website'], - ['store', null, 'test_store'], - ]; - - $requestPostMap = [['groups', null, $groups], ['config_state', null, 'test_config_state']]; - - $this->_requestMock->expects($this->any())->method('getPost')->will($this->returnValueMap($requestPostMap)); - $this->_requestMock->expects($this->any())->method('getParam')->will($this->returnValueMap($requestParamMap)); - - $backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); - $backendConfigMock->expects($this->once())->method('save'); - - $params = [ - 'section' => 'test_section', - 'website' => 'test_website', - 'store' => 'test_store', - 'groups' => $groups, - ]; - $this->_configFactoryMock->expects( - $this->once() - )->method( - 'create' - )->with( - ['data' => $params] - )->will( - $this->returnValue($backendConfigMock) - ); - - $this->assertEquals($this->resultRedirect, $this->_controller->execute()); - } - - public function testIndexActionSaveState() - { - $this->_sectionCheckerMock->expects($this->any())->method('isSectionAllowed')->will($this->returnValue(false)); - $inputData = [ - 'some_key' => 'some_value', - 'some_key_0' => '0', - 'some_key_1' => 'some_value_1', - ]; - $extraData = [ - 'some_key_0' => '0', - 'some_key_1' => '1', - ]; - - $userMock = $this->createMock(\Magento\User\Model\User::class); - $userMock->expects($this->once())->method('saveExtra')->with(['configState' => $extraData]); - $this->_authMock->expects($this->once())->method('getUser')->will($this->returnValue($userMock)); - $this->_requestMock->expects( - $this->any() - )->method( - 'getPost' - )->with( - 'config_state' - )->will( - $this->returnValue($inputData) - ); - - $this->assertEquals($this->resultRedirect, $this->_controller->execute()); - } - - public function testIndexActionGetGroupForSave() - { - $this->_sectionCheckerMock->expects($this->any())->method('isSectionAllowed')->will($this->returnValue(true)); - - $fixturePath = __DIR__ . '/_files/'; - $groups = require_once $fixturePath . 'groups_array.php'; - $requestParamMap = [ - ['section', null, 'test_section'], - ['website', null, 'test_website'], - ['store', null, 'test_store'], - ]; - - $requestPostMap = [['groups', null, $groups], ['config_state', null, 'test_config_state']]; - - $files = require_once $fixturePath . 'files_array.php'; - - $this->_requestMock->expects($this->any())->method('getPost')->will($this->returnValueMap($requestPostMap)); - $this->_requestMock->expects($this->any())->method('getParam')->will($this->returnValueMap($requestParamMap)); - $this->_requestMock->expects( - $this->once() - )->method( - 'getFiles' - )->with( - 'groups' - )->will( - $this->returnValue($files) - ); - - $groupToSave = require_once $fixturePath . 'expected_array.php'; - - $params = [ - 'section' => 'test_section', - 'website' => 'test_website', - 'store' => 'test_store', - 'groups' => $groupToSave, - ]; - $backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); - $this->_configFactoryMock->expects( - $this->once() - )->method( - 'create' - )->with( - ['data' => $params] - )->will( - $this->returnValue($backendConfigMock) - ); - $backendConfigMock->expects($this->once())->method('save'); - - $this->assertEquals($this->resultRedirect, $this->_controller->execute()); - } - - public function testIndexActionSaveAdvanced() - { - $this->_sectionCheckerMock->expects($this->any())->method('isSectionAllowed')->will($this->returnValue(true)); - - $requestParamMap = [ - ['section', null, 'advanced'], - ['website', null, 'test_website'], - ['store', null, 'test_store'], - ]; - - $this->_requestMock->expects($this->any())->method('getParam')->will($this->returnValueMap($requestParamMap)); - - $backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); - $this->_configFactoryMock->expects( - $this->once() - )->method( - 'create' - )->will( - $this->returnValue($backendConfigMock) - ); - $backendConfigMock->expects($this->once())->method('save'); - - $this->_cacheMock->expects($this->once())->method('clean')->with(\Zend_Cache::CLEANING_MODE_ALL); - $this->assertEquals($this->resultRedirect, $this->_controller->execute()); - } -} diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/expected_array.php b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/expected_array.php deleted file mode 100644 index a74fd9ef1eedf..0000000000000 --- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/expected_array.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -return [ - 'some.key' => 'some.val', - 'group.1' => [ - 'fields' => [ - 'f1.1' => ['value' => 'f1.1.val'], - 'f1.2' => ['value' => 'f1.2.val'], - 'g1.1' => ['value' => 'g1.1.val'], - ], - ], - 'group.2' => [ - 'fields' => ['f2.1' => ['value' => 'f2.1.val'], 'f2.2' => ['value' => 'f2.2.val']], - 'groups' => [ - 'group.2.1' => [ - 'fields' => [ - 'f2.1.1' => ['value' => 'f2.1.1.val'], - 'f2.1.2' => ['value' => 'f2.1.2.val'], - ], - 'groups' => [ - 'group.2.1.1' => [ - 'fields' => [ - 'f2.1.1.1' => ['value' => 'f2.1.1.1.val'], - 'f2.1.1.2' => ['value' => 'f2.1.1.2.val'], - ], - ], - ], - ], - ], - ] -]; diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/files_array.php b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/files_array.php deleted file mode 100644 index 3bc0f7a466733..0000000000000 --- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/files_array.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -return [ - 'group.1' => [ - 'fields' => ['f1.1' => ['value' => 'f1.1.val'], 'f1.2' => ['value' => 'f1.2.val']], - ], - 'group.2' => [ - 'fields' => [ - 'f2.1' => ['value' => 'f2.1.val'], - 'f2.2' => ['value' => 'f2.2.val'], - 'f2.3' => ['value' => ''], - ], - 'groups' => [ - 'group.2.1' => [ - 'fields' => [ - 'f2.1.1' => ['value' => 'f2.1.1.val'], - 'f2.1.2' => ['value' => 'f2.1.2.val'], - 'f2.1.3' => ['value' => ''], - ], - 'groups' => [ - 'group.2.1.1' => [ - 'fields' => [ - 'f2.1.1.1' => ['value' => 'f2.1.1.1.val'], - 'f2.1.1.2' => ['value' => 'f2.1.1.2.val'], - 'f2.1.1.3' => ['value' => ''], - ], - ], - ], - ], - ], - ], - 'group.3' => 'some.data', -]; diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php index a994532d5a69e..4874dc8ea03ae 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php @@ -98,6 +98,8 @@ public function __construct( } /** + * Return currency symbol. + * * @return string */ public function getCurrencySymbol() @@ -274,6 +276,8 @@ public function getImageUploadUrl() } /** + * Return product qty. + * * @param Product $product * @return float */ @@ -283,6 +287,8 @@ public function getProductStockQty(Product $product) } /** + * Return variation wizard. + * * @param array $initData * @return string */ @@ -298,6 +304,8 @@ public function getVariationWizard($initData) } /** + * Return product configuration matrix. + * * @return array|null */ public function getProductMatrix() @@ -309,6 +317,8 @@ public function getProductMatrix() } /** + * Return product attributes. + * * @return array|null */ public function getProductAttributes() @@ -316,10 +326,13 @@ public function getProductAttributes() if ($this->productAttributes === null) { $this->prepareVariations(); } + return $this->productAttributes; } /** + * Prepare product variations. + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @return void * TODO: move to class @@ -344,36 +357,13 @@ protected function prepareVariations() $price = $product->getPrice(); $variationOptions = []; foreach ($usedProductAttributes as $attribute) { - if (!isset($attributes[$attribute->getAttributeId()])) { - $attributes[$attribute->getAttributeId()] = [ - 'code' => $attribute->getAttributeCode(), - 'label' => $attribute->getStoreLabel(), - 'id' => $attribute->getAttributeId(), - 'position' => $configurableAttributes[$attribute->getAttributeId()]['position'], - 'chosen' => [], - ]; - foreach ($attribute->getOptions() as $option) { - if (!empty($option->getValue())) { - $attributes[$attribute->getAttributeId()]['options'][] = [ - 'attribute_code' => $attribute->getAttributeCode(), - 'attribute_label' => $attribute->getStoreLabel(0), - 'id' => $option->getValue(), - 'label' => $option->getLabel(), - 'value' => $option->getValue(), - ]; - } - } - } - $optionId = $variation[$attribute->getId()]['value']; - $variationOption = [ - 'attribute_code' => $attribute->getAttributeCode(), - 'attribute_label' => $attribute->getStoreLabel(0), - 'id' => $optionId, - 'label' => $variation[$attribute->getId()]['label'], - 'value' => $optionId, - ]; - $variationOptions[] = $variationOption; - $attributes[$attribute->getAttributeId()]['chosen'][] = $variationOption; + list($attributes, $variationOptions) = $this->prepareAttributes( + $attributes, + $attribute, + $configurableAttributes, + $variation, + $variationOptions + ); } $productMatrix[] = [ @@ -387,7 +377,8 @@ protected function prepareVariations() 'price' => $price, 'options' => $variationOptions, 'weight' => $product->getWeight(), - 'status' => $product->getStatus() + 'status' => $product->getStatus(), + '__disableTmpl' => true, ]; } } @@ -395,4 +386,57 @@ protected function prepareVariations() $this->productMatrix = $productMatrix; $this->productAttributes = array_values($attributes); } + + /** + * Prepare attributes. + * + * @param array $attributes + * @param object $attribute + * @param array $configurableAttributes + * @param array $variation + * @param array $variationOptions + * @return array + */ + private function prepareAttributes( + array $attributes, + $attribute, + array $configurableAttributes, + array $variation, + array $variationOptions + ): array { + if (!isset($attributes[$attribute->getAttributeId()])) { + $attributes[$attribute->getAttributeId()] = [ + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'id' => $attribute->getAttributeId(), + 'position' => $configurableAttributes[$attribute->getAttributeId()]['position'], + 'chosen' => [], + ]; + foreach ($attribute->getOptions() as $option) { + if (!empty($option->getValue())) { + $attributes[$attribute->getAttributeId()]['options'][] = [ + 'attribute_code' => $attribute->getAttributeCode(), + 'attribute_label' => $attribute->getStoreLabel(0), + 'id' => $option->getValue(), + 'label' => $option->getLabel(), + 'value' => $option->getValue(), + '__disableTmpl' => true, + ]; + } + } + } + $optionId = $variation[$attribute->getId()]['value']; + $variationOption = [ + 'attribute_code' => $attribute->getAttributeCode(), + 'attribute_label' => $attribute->getStoreLabel(0), + 'id' => $optionId, + 'label' => $variation[$attribute->getId()]['label'], + 'value' => $optionId, + '__disableTmpl' => true, + ]; + $variationOptions[] = $variationOption; + $attributes[$attribute->getAttributeId()]['chosen'][] = $variationOption; + + return [$attributes, $variationOptions]; + } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php index 4ead9ffe0fe70..b013916cc221a 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php @@ -1,7 +1,5 @@ <?php /** - * Catalog Configurable Product Attribute Model - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,9 +8,10 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; /** - * Configurable product attribute model. + * Catalog Configurable Product Attribute Model * * @method Attribute setProductAttribute(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $value) * @method \Magento\Eav\Model\Entity\Attribute\AbstractAttribute getProductAttribute() @@ -88,7 +87,7 @@ public function getOptions() } /** - * @inheritdoc + * @inheritDoc */ public function getLabel() { @@ -114,11 +113,11 @@ public function afterSave() } /** - * Load configurable attribute by product and product's attribute. + * Load configurable attribute by product and product's attribute * * @param \Magento\Catalog\Model\Product $product * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute - * @return void + * @throws LocalizedException */ public function loadByProductAndAttribute($product, $attribute) { @@ -146,7 +145,8 @@ public function deleteByProduct($product) } /** - * @inheritdoc + * @inheritDoc + * * @codeCoverageIgnore */ public function getAttributeId() @@ -155,7 +155,8 @@ public function getAttributeId() } /** - * @inheritdoc + * @inheritDoc + * * @codeCoverageIgnore */ public function getPosition() @@ -164,7 +165,8 @@ public function getPosition() } /** - * @inheritdoc + * @inheritDoc + * * @codeCoverageIgnore */ public function getIsUseDefault() @@ -173,7 +175,8 @@ public function getIsUseDefault() } /** - * @inheritdoc + * @inheritDoc + * * @codeCoverageIgnore */ public function getValues() @@ -184,7 +187,7 @@ public function getValues() //@codeCoverageIgnoreStart /** - * @inheritdoc + * @inheritDoc */ public function setAttributeId($attributeId) { @@ -192,7 +195,7 @@ public function setAttributeId($attributeId) } /** - * @inheritdoc + * @inheritDoc */ public function setLabel($label) { @@ -200,7 +203,7 @@ public function setLabel($label) } /** - * @inheritdoc + * @inheritDoc */ public function setPosition($position) { @@ -208,7 +211,7 @@ public function setPosition($position) } /** - * @inheritdoc + * @inheritDoc */ public function setIsUseDefault($isUseDefault) { @@ -216,7 +219,7 @@ public function setIsUseDefault($isUseDefault) } /** - * @inheritdoc + * @inheritDoc */ public function setValues(array $values = null) { @@ -224,9 +227,7 @@ public function setValues(array $values = null) } /** - * @inheritdoc - * - * @return \Magento\ConfigurableProduct\Api\Data\OptionExtensionInterface|null + * @inheritDoc */ public function getExtensionAttributes() { @@ -234,10 +235,7 @@ public function getExtensionAttributes() } /** - * @inheritdoc - * - * @param \Magento\ConfigurableProduct\Api\Data\OptionExtensionInterface $extensionAttributes - * @return $this + * @inheritDoc */ public function setExtensionAttributes( \Magento\ConfigurableProduct\Api\Data\OptionExtensionInterface $extensionAttributes @@ -246,7 +244,7 @@ public function setExtensionAttributes( } /** - * @inheritdoc + * @inheritDoc */ public function getProductId() { @@ -254,7 +252,7 @@ public function getProductId() } /** - * @inheritdoc + * @inheritDoc */ public function setProductId($value) { @@ -265,14 +263,9 @@ public function setProductId($value) /** * @inheritdoc - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return array_diff( parent::__sleep(), ['metadataPool'] @@ -281,14 +274,9 @@ public function __sleep() /** * @inheritdoc - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->metadataPool = $objectManager->get(MetadataPool::class); diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php index 81cbbd06c523c..8f2cc6ddb43ce 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php @@ -1,7 +1,5 @@ <?php /** - * Catalog Configurable Product Attribute Collection - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -18,7 +16,7 @@ use Magento\Catalog\Api\Data\ProductInterface; /** - * Collection of configurable product attributes. + * Catalog Configurable Product Attribute Collection * * @api * @SuppressWarnings(PHPMD.LongVariable) @@ -304,9 +302,7 @@ protected function _loadLabels() } /** - * Load related options' data. - * - * @return void + * Load attribute options. */ protected function loadOptions() { @@ -344,6 +340,7 @@ protected function loadOptions() * @param \Magento\Catalog\Model\Product[] $usedProducts * @param AbstractAttribute $productAttribute * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ protected function getIncludedOptions(array $usedProducts, AbstractAttribute $productAttribute) { @@ -358,14 +355,9 @@ protected function getIncludedOptions(array $usedProducts, AbstractAttribute $pr /** * @inheritdoc * @since 100.0.6 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return array_diff( parent::__sleep(), [ @@ -382,14 +374,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.6 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = ObjectManager::getInstance(); $this->_storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 45a4323e7f4bc..91f7ee5eb3209 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -10,7 +10,7 @@ type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, } type ConfigurableVariant @doc(description: "An array containing all the simple product variants of a configurable product") { - attributes: [ConfigurableAttributeOption] @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes") @doc(description: "") + attributes: [ConfigurableAttributeOption] @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes") @doc(description: "") product: SimpleProduct @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") } diff --git a/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml b/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml new file mode 100644 index 0000000000000..3ef941fa2e0ce --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectContact"> + <annotations> + <features value="Contact"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Contact Pages"/> + <description value="Verify that the Secure URL configuration applies to the Contact pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15539"/> + <group value="contact"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/contact" stepKey="goToUnsecureContactURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/contact" stepKey="seeSecureContactURL"/> + <amOnUrl url="http://{$hostname}/contact/index/post" stepKey="goToUnsecureContactFormURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/contact/index/post" stepKey="seeSecureContactFormURL"/> + </test> +</tests> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml index 9248337e5c413..397c2598dc3b0 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml @@ -3,8 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<?php + /** * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currencysymbol */ @@ -27,7 +26,7 @@ <div class="admin__field admin__field-option"> <input id="custom_currency_symbol_inherit<?= $block->escapeHtmlAttr($code) ?>" class="admin__control-checkbox" type="checkbox" - onclick="toggleUseDefault(<?= '\'' . $block->escapeJs($code) . '\',\'' . $block->escapeJs($data['parentSymbol']) . '\'' ?>)" + onclick="toggleUseDefault(<?= '\'' . $block->escapeHtmlAttr($block->escapeJs($code)) . '\',\'' . $block->escapeJs($data['parentSymbol']) . '\'' ?>)" <?= $data['inherited'] ? ' checked="checked"' : '' ?> value="1" name="inherit_custom_currency_symbol[<?= $block->escapeHtmlAttr($code) ?>]"> @@ -43,25 +42,10 @@ <?php endforeach; ?> </fieldset> </form> -<script> -require(['jquery', "mage/mage", 'prototype'], function(jQuery){ - - jQuery('#currency-symbols-form').mage('form').mage('validation'); - - function toggleUseDefault(code, value) +<script type="text/x-magento-init"> { - checkbox = jQuery('#custom_currency_symbol_inherit'+code); - input = jQuery('#custom_currency_symbol'+code); - - if (checkbox.is(':checked')) { - input.addClass('disabled'); - input.val(value); - input.prop('readonly', true); - } else { - input.removeClass('disabled'); - input.prop('readonly', false); + "#currency-symbols-form": { + "Magento_CurrencySymbol/js/symbols-form": {} } } - window.toggleUseDefault = toggleUseDefault; -}); </script> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/web/js/symbols-form.js b/app/code/Magento/CurrencySymbol/view/adminhtml/web/js/symbols-form.js new file mode 100644 index 0000000000000..68f914ddb1b4d --- /dev/null +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/web/js/symbols-form.js @@ -0,0 +1,39 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/mage' +], function ($) { + 'use strict'; + + return function (config, element) { + $(element) + .mage('form') + .mage('validation'); + + /** + * Toggle the field to use the default value + * + * @param {String} code + * @param {String} value + */ + function toggleUseDefault(code, value) { + var checkbox = $('#custom_currency_symbol_inherit' + code), + input = $('#custom_currency_symbol' + code); + + if (checkbox.is(':checked')) { + input.addClass('disabled'); + input.val(value); + input.prop('readonly', true); + } else { + input.removeClass('disabled'); + input.prop('readonly', false); + } + } + + window.toggleUseDefault = toggleUseDefault; + }; +}); diff --git a/app/code/Magento/Customer/Console/Command/UpgradeHashAlgorithmCommand.php b/app/code/Magento/Customer/Console/Command/UpgradeHashAlgorithmCommand.php index dd8dec0b94c15..c980fe1fe7769 100644 --- a/app/code/Magento/Customer/Console/Command/UpgradeHashAlgorithmCommand.php +++ b/app/code/Magento/Customer/Console/Command/UpgradeHashAlgorithmCommand.php @@ -13,6 +13,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * Upgrade users passwords to the new algorithm + */ class UpgradeHashAlgorithmCommand extends Command { /** @@ -65,8 +68,11 @@ protected function execute(InputInterface $input, OutputInterface $output) $customer->load($customer->getId()); if (!$this->encryptor->validateHashVersion($customer->getPasswordHash())) { list($hash, $salt, $version) = explode(Encryptor::DELIMITER, $customer->getPasswordHash(), 3); - $version .= Encryptor::DELIMITER . Encryptor::HASH_VERSION_LATEST; - $customer->setPasswordHash($this->encryptor->getHash($hash, $salt, $version)); + $version .= Encryptor::DELIMITER . $this->encryptor->getLatestHashVersion(); + $hash = $this->encryptor->getHash($hash, $salt, $this->encryptor->getLatestHashVersion()); + list($hash, $salt) = explode(Encryptor::DELIMITER, $hash, 3); + $hash = implode(Encryptor::DELIMITER, [$hash, $salt, $version]); + $customer->setPasswordHash($hash); $customer->save(); $output->write("."); } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php index 7549315f9ffcd..5ffce4cbcd989 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -11,6 +10,9 @@ use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\GroupRepositoryInterface; +/** + * Controller class Save. Performs save action of customers group + */ class Save extends \Magento\Customer\Controller\Adminhtml\Group implements HttpPostActionInterface { /** @@ -79,6 +81,7 @@ public function execute() $resultRedirect = $this->resultRedirectFactory->create(); try { $customerGroupCode = (string)$this->getRequest()->getParam('code'); + if ($id !== null) { $customerGroup = $this->groupRepository->getById((int)$id); $customerGroupCode = $customerGroupCode ?: $customerGroup->getCode(); diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index 15d98af86b72e..24899a1c5979c 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -579,7 +579,7 @@ public function authenticate($username, $password) } try { $this->getAuthentication()->authenticate($customerId, $password); - // phpcs:disable Magento2.Exceptions.ThrowCatch + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InvalidEmailOrPasswordException $e) { throw new InvalidEmailOrPasswordException(__('Invalid login or password.')); } @@ -890,7 +890,7 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash throw new InputMismatchException( __('A customer with the same email address already exists in an associated website.') ); - // phpcs:disable Magento2.Exceptions.ThrowCatch + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (LocalizedException $e) { throw $e; } @@ -910,7 +910,7 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash } } $this->customerRegistry->remove($customer->getId()); - // phpcs:disable Magento2.Exceptions.ThrowCatch + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InputException $e) { $this->customerRepository->delete($customer); throw $e; @@ -1017,7 +1017,7 @@ private function changePasswordForCustomer($customer, $currentPassword, $newPass { try { $this->getAuthentication()->authenticate($customer->getId(), $currentPassword); - // phpcs:disable Magento2.Exceptions.ThrowCatch + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InvalidEmailOrPasswordException $e) { throw new InvalidEmailOrPasswordException( __("The password doesn't match this account. Verify the password and try again.") @@ -1549,7 +1549,7 @@ protected function getFullCustomerObject($customer) */ public function getPasswordHash($password) { - return $this->encryptor->getHash($password); + return $this->encryptor->getHash($password, true); } /** diff --git a/app/code/Magento/Customer/Model/Attribute.php b/app/code/Magento/Customer/Model/Attribute.php index ae714f993082e..98a97872f15f4 100644 --- a/app/code/Magento/Customer/Model/Attribute.php +++ b/app/code/Magento/Customer/Model/Attribute.php @@ -202,14 +202,9 @@ public function canBeFilterableInGrid() /** * @inheritdoc - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->unsetData('entity_type'); return array_diff( parent::__sleep(), @@ -219,14 +214,9 @@ public function __sleep() /** * @inheritdoc - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->indexerRegistry = $objectManager->get(\Magento\Framework\Indexer\IndexerRegistry::class); diff --git a/app/code/Magento/Customer/Model/Customer/Attribute/Source/Group.php b/app/code/Magento/Customer/Model/Customer/Attribute/Source/Group.php index e590e5e213acd..296d2877df8ea 100644 --- a/app/code/Magento/Customer/Model/Customer/Attribute/Source/Group.php +++ b/app/code/Magento/Customer/Model/Customer/Attribute/Source/Group.php @@ -48,7 +48,15 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) { if (!$this->_options) { $groups = $this->_groupManagement->getLoggedInGroups(); + $this->_options = $this->_converter->toOptionArray($groups, 'id', 'code'); + + array_walk( + $this->_options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); } return $this->_options; diff --git a/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php b/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php index 31e0e2727436b..664f85f841e3f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php @@ -109,7 +109,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Customer\Api\Data\GroupInterface $group) { @@ -165,7 +165,7 @@ public function save(\Magento\Customer\Api\Data\GroupInterface $group) } /** - * {@inheritdoc} + * @inheritdoc */ public function getById($id) { @@ -179,7 +179,7 @@ public function getById($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { @@ -301,6 +301,7 @@ public function deleteById($id) * * @param \Magento\Customer\Api\Data\GroupInterface $group * @throws InputException + * @throws \Zend_Validate_Exception * @return void * * @SuppressWarnings(PHPMD.NPathComplexity) diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml index 2b88657c6ca2b..d7372b07de14b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml @@ -38,6 +38,30 @@ <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct1"> <requiredEntity createDataKey="createSimpleCategory1"/> </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption"/> + <requiredEntity createDataKey="createSimpleCategory1"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct1"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild"> + <requiredEntity createDataKey="createConfigProduct1"/> + <requiredEntity createDataKey="createConfigChildProduct"/> + </createData> <!-- Create Virtual Product --> <createData entity="VirtualProduct" stepKey="createVirtualProduct1"> @@ -77,6 +101,9 @@ <requiredEntity createDataKey="createDownloadableProduct1"/> </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- Login --> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> </before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml new file mode 100644 index 0000000000000..da9dddf0539d3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectCustomer"> + <annotations> + <features value="Customer"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Customer Pages"/> + <description value="Verify that the Secure URL configuration applies to the Customer pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15618"/> + <group value="customer"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <amOnPage url="/" stepKey="goToHomePage"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/customer" stepKey="goToUnsecureCustomerURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/customer" stepKey="seeSecureCustomerURL"/> + <amOnUrl url="http://{$hostname}/customer/section/load" stepKey="goToUnsecureCustomerSectionLoadURL"/> + <seeCurrentUrlEquals url="http://{$hostname}/customer/section/load" stepKey="seeUnsecureCustomerSectionLoadURL"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index 5eda0c52c1db2..3c38cd0f7b4e2 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -816,14 +816,18 @@ public function testCreateAccountWithPasswordInputException( if ($testNumber == 1) { $this->expectException(\Magento\Framework\Exception\InputException::class); - $this->expectExceptionMessage('The password needs at least ' . $minPasswordLength . ' characters. ' - . 'Create a new password and try again.'); + $this->expectExceptionMessage( + 'The password needs at least ' . $minPasswordLength . ' characters. ' + . 'Create a new password and try again.' + ); } if ($testNumber == 2) { $this->expectException(\Magento\Framework\Exception\InputException::class); - $this->expectExceptionMessage('Minimum of different classes of characters in password is ' . - $minCharacterSetsNum . '. Classes of characters: Lower Case, Upper Case, Digits, Special Characters.'); + $this->expectExceptionMessage( + 'Minimum of different classes of characters in password is ' . + $minCharacterSetsNum . '. Classes of characters: Lower Case, Upper Case, Digits, Special Characters.' + ); } $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); @@ -1269,7 +1273,7 @@ public function testInitiatePasswordResetEmailReminder() $storeId = 1; - $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); + $hash = hash("sha256", uniqid(microtime() . random_int(0, PHP_INT_MAX), true)); $this->emailNotificationMock->expects($this->once()) ->method('passwordReminder') @@ -1293,7 +1297,7 @@ public function testInitiatePasswordResetEmailReset() $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; - $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); + $hash = hash("sha256", uniqid(microtime() . random_int(0, PHP_INT_MAX), true)); $this->emailNotificationMock->expects($this->once()) ->method('passwordResetConfirmation') @@ -1317,7 +1321,7 @@ public function testInitiatePasswordResetNoTemplate() $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; - $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); + $hash = hash("sha256", uniqid(microtime() . random_int(0, PHP_INT_MAX), true)); $this->prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash); diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/ColumnFactoryTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/ColumnFactoryTest.php index 131b1ee94cc14..d917cc4908ac8 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/ColumnFactoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/ColumnFactoryTest.php @@ -7,6 +7,9 @@ use Magento\Customer\Ui\Component\ColumnFactory; +/** + * Test ColumnFactory Class + */ class ColumnFactoryTest extends \PHPUnit\Framework\TestCase { /** @var \Magento\Customer\Api\Data\OptionInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -90,6 +93,7 @@ public function testCreate() ] ], 'component' => 'Magento_Ui/js/grid/columns/column', + '__disableTmpl' => 'true' ], ], 'context' => $this->context, diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/FilterFactoryTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/FilterFactoryTest.php index 7fbf9d2a2a10a..f3c0a56262622 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/FilterFactoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/FilterFactoryTest.php @@ -7,6 +7,9 @@ use Magento\Customer\Ui\Component\FilterFactory; +/** + * Test FilterFactory Class + */ class FilterFactoryTest extends \PHPUnit\Framework\TestCase { /** @var \Magento\Customer\Api\Data\OptionInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -69,6 +72,7 @@ public function testCreate() 'config' => [ 'dataScope' => $filterName, 'label' => __('Label'), + '__disableTmpl' => 'true', 'options' => [['value' => 'Value', 'label' => 'Label']], 'caption' => __('Select...'), ], diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/AttributeRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/AttributeRepositoryTest.php index 0662235c0d5ac..c12dec865cde8 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/AttributeRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/AttributeRepositoryTest.php @@ -7,6 +7,9 @@ use Magento\Customer\Ui\Component\Listing\AttributeRepository; +/** + * Test AttributeRepository Class + */ class AttributeRepositoryTest extends \PHPUnit\Framework\TestCase { /** @var \Magento\Customer\Api\CustomerMetadataManagementInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -144,7 +147,8 @@ public function testGetList() 'options' => [ [ 'label' => 'Label', - 'value' => 'Value' + 'value' => 'Value', + '__disableTmpl' => true ] ], 'is_used_in_grid' => true, diff --git a/app/code/Magento/Customer/Ui/Component/ColumnFactory.php b/app/code/Magento/Customer/Ui/Component/ColumnFactory.php index 60bf3ea26b78c..cb66dc3db7c77 100644 --- a/app/code/Magento/Customer/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Customer/Ui/Component/ColumnFactory.php @@ -9,6 +9,9 @@ use Magento\Customer\Ui\Component\Listing\Column\InlineEditUpdater; use Magento\Customer\Api\CustomerMetadataInterface; +/** + * Class ColumnFactory. Responsible for the column object generation + */ class ColumnFactory { /** @@ -55,6 +58,8 @@ public function __construct( } /** + * Creates column object for grid ui component + * * @param array $attributeData * @param string $columnName * @param \Magento\Framework\View\Element\UiComponent\ContextInterface $context @@ -63,13 +68,19 @@ public function __construct( */ public function create(array $attributeData, $columnName, $context, array $config = []) { - $config = array_merge([ - 'label' => __($attributeData[AttributeMetadata::FRONTEND_LABEL]), - 'dataType' => $this->getDataType($attributeData[AttributeMetadata::FRONTEND_INPUT]), - 'align' => 'left', - 'visible' => (bool)$attributeData[AttributeMetadata::IS_VISIBLE_IN_GRID], - 'component' => $this->getJsComponent($this->getDataType($attributeData[AttributeMetadata::FRONTEND_INPUT])), - ], $config); + $config = array_merge( + [ + 'label' => __($attributeData[AttributeMetadata::FRONTEND_LABEL]), + 'dataType' => $this->getDataType($attributeData[AttributeMetadata::FRONTEND_INPUT]), + 'align' => 'left', + 'visible' => (bool)$attributeData[AttributeMetadata::IS_VISIBLE_IN_GRID], + 'component' => $this->getJsComponent( + $this->getDataType($attributeData[AttributeMetadata::FRONTEND_INPUT]) + ), + '__disableTmpl' => 'true' + ], + $config + ); if ($attributeData[AttributeMetadata::FRONTEND_INPUT] == 'date') { $config['dateFormat'] = 'MMM d, y'; $config['timezone'] = false; @@ -101,6 +112,8 @@ public function create(array $attributeData, $columnName, $context, array $confi } /** + * Returns component map + * * @param string $dataType * @return string */ @@ -110,6 +123,8 @@ protected function getJsComponent($dataType) } /** + * Returns component map depends on data type + * * @param string $frontendType * @return string */ diff --git a/app/code/Magento/Customer/Ui/Component/FilterFactory.php b/app/code/Magento/Customer/Ui/Component/FilterFactory.php index 9d8fcdb9715ca..9bf07b877cc07 100644 --- a/app/code/Magento/Customer/Ui/Component/FilterFactory.php +++ b/app/code/Magento/Customer/Ui/Component/FilterFactory.php @@ -7,6 +7,9 @@ use Magento\Customer\Api\Data\AttributeMetadataInterface as AttributeMetadata; +/** + * Class FilterFactory. Responsible for generation filter object + */ class FilterFactory { /** @@ -34,6 +37,8 @@ public function __construct(\Magento\Framework\View\Element\UiComponentFactory $ } /** + * Creates filter object + * * @param array $attributeData * @param \Magento\Framework\View\Element\UiComponent\ContextInterface $context * @return \Magento\Ui\Component\Listing\Columns\ColumnInterface @@ -43,6 +48,7 @@ public function create(array $attributeData, $context) $config = [ 'dataScope' => $attributeData[AttributeMetadata::ATTRIBUTE_CODE], 'label' => __($attributeData[AttributeMetadata::FRONTEND_LABEL]), + '__disableTmpl' => 'true' ]; if ($attributeData[AttributeMetadata::OPTIONS]) { $config['options'] = $attributeData[AttributeMetadata::OPTIONS]; @@ -63,6 +69,8 @@ public function create(array $attributeData, $context) } /** + * Returns filter type + * * @param string $frontendInput * @return string */ diff --git a/app/code/Magento/Customer/Ui/Component/Listing/AttributeRepository.php b/app/code/Magento/Customer/Ui/Component/Listing/AttributeRepository.php index d0af1ec21467f..eb8359de93f32 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/AttributeRepository.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/AttributeRepository.php @@ -13,6 +13,9 @@ use Magento\Customer\Api\MetadataManagementInterface; use Magento\Customer\Model\Indexer\Attribute\Filter; +/** + * Class AttributeRepository + */ class AttributeRepository { const BILLING_ADDRESS_PREFIX = 'billing_'; @@ -69,6 +72,8 @@ public function __construct( } /** + * Returns attribute list for current customer + * * @return array */ public function getList() @@ -93,6 +98,8 @@ public function getList() } /** + * Returns attribute list for given entity type code + * * @param AttributeMetadataInterface[] $metadata * @param string $entityTypeCode * @param MetadataManagementInterface $management @@ -136,12 +143,18 @@ protected function getOptionArray(array $options) { /** @var \Magento\Customer\Api\Data\OptionInterface $option */ foreach ($options as &$option) { - $option = ['label' => (string)$option->getLabel(), 'value' => $option->getValue()]; + $option = [ + 'label' => (string)$option->getLabel(), + 'value' => $option->getValue(), + '__disableTmpl' => true + ]; } return $options; } /** + * Return customer group's metadata by given group code + * * @param string $code * @return [] */ diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/Group/Options.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/Group/Options.php index f521a95e1e616..61cb06cf77e0d 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/Group/Options.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/Group/Options.php @@ -43,6 +43,14 @@ public function toOptionArray() if ($this->options === null) { $this->options = $this->collectionFactory->create()->toOptionArray(); } + + array_walk( + $this->options, + function (&$item) { + $item['__disableTmpl'] = true; + } + ); + return $this->options; } } diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php index 00c5f99fab46c..6870bd1136d10 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php @@ -96,8 +96,11 @@ public function prepareDataSource(array $dataSource) ), 'label' => __('Delete'), 'confirm' => [ - 'title' => __('Delete %1', $title), - 'message' => __('Are you sure you want to delete a %1 record?', $title) + 'title' => __('Delete %1', $this->escaper->escapeJs($title)), + 'message' => __( + 'Are you sure you want to delete a %1 record?', + $this->escaper->escapeJs($title) + ) ], 'post' => true ]; diff --git a/app/code/Magento/Customer/Ui/Component/MassAction/Group/Options.php b/app/code/Magento/Customer/Ui/Component/MassAction/Group/Options.php index 146adacac9553..e5739317bca8d 100644 --- a/app/code/Magento/Customer/Ui/Component/MassAction/Group/Options.php +++ b/app/code/Magento/Customer/Ui/Component/MassAction/Group/Options.php @@ -88,6 +88,7 @@ public function jsonSerialize() $this->options[$optionCode['value']] = [ 'type' => 'customer_group_' . $optionCode['value'], 'label' => __($optionCode['label']), + '__disableTmpl' => true ]; if ($this->urlPath && $this->paramName) { diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml index 86e5852d67aeb..8a4d07d2bc4f3 100644 --- a/app/code/Magento/Customer/etc/adminhtml/system.xml +++ b/app/code/Magento/Customer/etc/adminhtml/system.xml @@ -280,6 +280,7 @@ </field> <field id="html" translate="label" type="textarea" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>HTML</label> + <comment>Only 'b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul' tags are allowed</comment> </field> <field id="pdf" translate="label" type="textarea" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>PDF</label> diff --git a/app/code/Magento/Customer/etc/webapi.xml b/app/code/Magento/Customer/etc/webapi.xml index c536e26bcc82a..38717619406aa 100644 --- a/app/code/Magento/Customer/etc/webapi.xml +++ b/app/code/Magento/Customer/etc/webapi.xml @@ -134,7 +134,7 @@ <resource ref="Magento_Customer::manage"/> </resources> </route> - <route url="/V1/customers/me" method="PUT"> + <route url="/V1/customers/me" method="PUT" soapOperation="saveSelf"> <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="save"/> <resources> <resource ref="self"/> @@ -143,7 +143,7 @@ <parameter name="customer.id" force="true">%customer_id%</parameter> </data> </route> - <route url="/V1/customers/me" method="GET"> + <route url="/V1/customers/me" method="GET" soapOperation="getSelf"> <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="getById"/> <resources> <resource ref="self"/> @@ -244,7 +244,7 @@ <resource ref="Magento_Customer::manage"/> </resources> </route> - <route url="/V1/customers/me/billingAddress" method="GET"> + <route url="/V1/customers/me/billingAddress" method="GET" soapOperation="getMyDefaultBillingAddress"> <service class="Magento\Customer\Api\AccountManagementInterface" method="getDefaultBillingAddress"/> <resources> <resource ref="self"/> @@ -259,7 +259,7 @@ <resource ref="Magento_Customer::manage"/> </resources> </route> - <route url="/V1/customers/me/shippingAddress" method="GET"> + <route url="/V1/customers/me/shippingAddress" method="GET" soapOperation="getMyDefaultShippingAddress"> <service class="Magento\Customer\Api\AccountManagementInterface" method="getDefaultShippingAddress"/> <resources> <resource ref="self"/> diff --git a/app/code/Magento/Customer/i18n/en_US.csv b/app/code/Magento/Customer/i18n/en_US.csv index e1c68f3d81e9d..3495feb925cb3 100644 --- a/app/code/Magento/Customer/i18n/en_US.csv +++ b/app/code/Magento/Customer/i18n/en_US.csv @@ -500,6 +500,7 @@ Strong,Strong "Address Templates","Address Templates" "Online Customers Options","Online Customers Options" "Online Minutes Interval","Online Minutes Interval" +"Only 'b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul' tags are allowed","Only 'b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul' tags are allowed" "Leave empty for default (15 minutes).","Leave empty for default (15 minutes)." "Customer Notification","Customer Notification" "Customer Grid","Customer Grid" diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml index b3baeace89731..a0e7a2faefbab 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/view/personal_info.phtml @@ -11,6 +11,7 @@ $lastLoginDateStore = $block->getStoreLastLoginDate(); $createDateAdmin = $block->getCreateDate(); $createDateStore = $block->getStoreCreateDate(); +$allowedAddressHtmlTags = ['b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul']; ?> <div class="fieldset-wrapper customer-information"> <div class="fieldset-wrapper-title"> @@ -59,7 +60,7 @@ $createDateStore = $block->getStoreCreateDate(); </table> <address> <strong><?= $block->escapeHtml(__('Default Billing Address')) ?></strong><br/> - <?= $block->getBillingAddressHtml() ?> + <?= $block->escapeHtml($block->getBillingAddressHtml(), $allowedAddressHtmlTags) ?> </address> </div> diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js index 66ef89c9413c7..42f427cf8a094 100644 --- a/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js +++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js @@ -20,6 +20,11 @@ define([ }, listens: { action: 'onAction' + }, + ignoreTmpls: { + fieldAction: true, + options: true, + action: true } }, diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index 1238184075057..bf7527c855e57 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -126,8 +126,8 @@ type CustomerAddressRegion @doc(description: "CustomerAddressRegion defines the } type CustomerAddressAttribute { - attribute_code: String @doc(description: "Attribute code") - value: String @doc(description: "Attribute value") + attribute_code: String @doc(description: "Attribute code") + value: String @doc(description: "Attribute value") } type IsEmailAvailableOutput { diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index e765a81554670..5959294fe6dc7 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -1088,7 +1088,7 @@ function () use ($deferredResponses, $responseBodies) { protected function _getQuotesFromServer($request) { $client = $this->_httpClientFactory->create(); - $client->setUri((string)$this->getConfigData('gateway_url')); + $client->setUri($this->getGatewayURL()); $client->setConfig(['maxredirects' => 0, 'timeout' => 30]); $client->setRawData(utf8_encode($request)); @@ -1410,7 +1410,7 @@ public function proccessAdditionalValidation(\Magento\Framework\DataObject $requ public function processAdditionalValidation(\Magento\Framework\DataObject $request) { //Skip by item validation if there is no items in request - if (!count($this->getAllItems($request))) { + if (empty($this->getAllItems($request))) { $this->_errors[] = __('There is no items in this order'); } @@ -1681,7 +1681,7 @@ protected function _doRequest() try { $response = $this->httpClient->request( new Request( - (string)$this->getConfigData('gateway_url'), + $this->getGatewayURL(), Request::METHOD_POST, ['Content-Type' => 'application/xml'], $request @@ -1850,7 +1850,7 @@ protected function _getXMLTracking($trackings) try { $response = $this->httpClient->request( new Request( - (string)$this->getConfigData('gateway_url'), + $this->getGatewayURL(), Request::METHOD_POST, ['Content-Type' => 'application/xml'], $request @@ -1883,7 +1883,7 @@ protected function _parseXmlTrackingResponse($trackings, $response) $errorTitle = __('Unable to retrieve tracking'); $resultArr = []; - if (strlen(trim($response)) > 0) { + if (!empty(trim($response))) { $xml = $this->parseXml($response, \Magento\Shipping\Model\Simplexml\Element::class); if (!is_object($xml)) { $errorTitle = __('Response is in the wrong format'); @@ -2131,4 +2131,18 @@ private function buildSoftwareVersion(): string { return substr($this->productMetadata->getVersion(), 0, 10); } + + /** + * Get the gateway URL + * + * @return string + */ + private function getGatewayURL(): string + { + if ($this->getConfigData('sandbox_mode')) { + return (string)$this->getConfigData('sandbox_url'); + } else { + return (string)$this->getConfigData('gateway_url'); + } + } } diff --git a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php index 0fde6fa8e9d13..d1b35c8e2b77f 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php @@ -446,6 +446,50 @@ public function buildSoftwareVersionProvider() ]; } + /** + * Tests if the DHL client returns the appropriate API URL. + * + * @dataProvider getGatewayURLProvider + * @param $sandboxMode + * @param $expectedURL + * @throws \ReflectionException + */ + public function testGetGatewayURL($sandboxMode, $expectedURL) + { + $scopeConfigValueMap = [ + ['carriers/dhl/gateway_url', 'store', null, 'https://xmlpi-ea.dhl.com/XMLShippingServlet'], + ['carriers/dhl/sandbox_url', 'store', null, 'https://xmlpitest-ea.dhl.com/XMLShippingServlet'], + ['carriers/dhl/sandbox_mode', 'store', null, $sandboxMode] + ]; + + $this->scope->method('getValue') + ->willReturnMap($scopeConfigValueMap); + + $this->model = $this->objectManager->getObject( + Carrier::class, + [ + 'scopeConfig' => $this->scope + ] + ); + + $method = new \ReflectionMethod($this->model, 'getGatewayURL'); + $method->setAccessible(true); + $this->assertEquals($expectedURL, $method->invoke($this->model)); + } + + /** + * Data provider for testGetGatewayURL + * + * @return array + */ + public function getGatewayURLProvider() + { + return [ + 'standard_url' => [0, 'https://xmlpi-ea.dhl.com/XMLShippingServlet'], + 'sandbox_url' => [1, 'https://xmlpitest-ea.dhl.com/XMLShippingServlet'] + ]; + } + /** * Creates mock for XML factory. * diff --git a/app/code/Magento/Dhl/etc/adminhtml/system.xml b/app/code/Magento/Dhl/etc/adminhtml/system.xml index 7ab37de2f3658..597f33b579282 100644 --- a/app/code/Magento/Dhl/etc/adminhtml/system.xml +++ b/app/code/Magento/Dhl/etc/adminhtml/system.xml @@ -14,9 +14,6 @@ <label>Enabled for Checkout</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="gateway_url" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> - <label>Gateway URL</label> - </field> <field id="title" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Title</label> </field> @@ -145,6 +142,10 @@ <label>Debug</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="sandbox_mode" translate="label" type="select" sortOrder="1960" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Sandbox Mode</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> </section> </system> diff --git a/app/code/Magento/Dhl/etc/config.xml b/app/code/Magento/Dhl/etc/config.xml index 79addefb34a16..b46152fb0ecad 100644 --- a/app/code/Magento/Dhl/etc/config.xml +++ b/app/code/Magento/Dhl/etc/config.xml @@ -25,6 +25,7 @@ <doc_methods>2,5,6,7,9,B,C,D,U,K,L,G,W,I,N,O,R,S,T,X</doc_methods> <free_method>G</free_method> <gateway_url>https://xmlpi-ea.dhl.com/XMLShippingServlet</gateway_url> + <sandbox_url>https://xmlpitest-ea.dhl.com/XMLShippingServlet</sandbox_url> <id backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> <password backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> <content_type>N</content_type> diff --git a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php index f40df744dd3ea..c0bc825a8285b 100644 --- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php +++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php @@ -7,7 +7,9 @@ namespace Magento\Downloadable\Controller\Download; +use Magento\Catalog\Model\Product\SalabilityChecker; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Framework\App\Action\Context; use Magento\Framework\App\ResponseInterface; /** @@ -18,7 +20,24 @@ class LinkSample extends \Magento\Downloadable\Controller\Download { /** - * Download link's sample action + * @var SalabilityChecker + */ + private $salabilityChecker; + + /** + * @param Context $context + * @param SalabilityChecker|null $salabilityChecker + */ + public function __construct( + Context $context, + SalabilityChecker $salabilityChecker = null + ) { + parent::__construct($context); + $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class); + } + + /** + * Download link's sample action. * * @return ResponseInterface */ @@ -27,7 +46,7 @@ public function execute() $linkId = $this->getRequest()->getParam('link_id', 0); /** @var \Magento\Downloadable\Model\Link $link */ $link = $this->_objectManager->create(\Magento\Downloadable\Model\Link::class)->load($linkId); - if ($link->getId()) { + if ($link->getId() && $this->salabilityChecker->isSalable($link->getProductId())) { $resource = ''; $resourceType = ''; if ($link->getSampleType() == DownloadHelper::LINK_TYPE_URL) { @@ -52,6 +71,7 @@ public function execute() ); } } + return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); } } diff --git a/app/code/Magento/Downloadable/Controller/Download/Sample.php b/app/code/Magento/Downloadable/Controller/Download/Sample.php index ac9eeac678f8d..b95ec510fdd9b 100644 --- a/app/code/Magento/Downloadable/Controller/Download/Sample.php +++ b/app/code/Magento/Downloadable/Controller/Download/Sample.php @@ -7,7 +7,9 @@ namespace Magento\Downloadable\Controller\Download; +use Magento\Catalog\Model\Product\SalabilityChecker; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Framework\App\Action\Context; use Magento\Framework\App\ResponseInterface; /** @@ -18,7 +20,24 @@ class Sample extends \Magento\Downloadable\Controller\Download { /** - * Download sample action + * @var SalabilityChecker + */ + private $salabilityChecker; + + /** + * @param Context $context + * @param SalabilityChecker|null $salabilityChecker + */ + public function __construct( + Context $context, + SalabilityChecker $salabilityChecker = null + ) { + parent::__construct($context); + $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class); + } + + /** + * Download sample action. * * @return ResponseInterface */ @@ -27,7 +46,7 @@ public function execute() $sampleId = $this->getRequest()->getParam('sample_id', 0); /** @var \Magento\Downloadable\Model\Sample $sample */ $sample = $this->_objectManager->create(\Magento\Downloadable\Model\Sample::class)->load($sampleId); - if ($sample->getId()) { + if ($sample->getId() && $this->salabilityChecker->isSalable($sample->getProductId())) { $resource = ''; $resourceType = ''; if ($sample->getSampleType() == DownloadHelper::LINK_TYPE_URL) { @@ -49,6 +68,7 @@ public function execute() ); } } + return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); } } diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml new file mode 100644 index 0000000000000..6e039ca413a08 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml @@ -0,0 +1,44 @@ +<?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="StorefrontVerifySecureURLRedirectDownloadable"> + <annotations> + <features value="Downloadable"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Downloadable Pages"/> + <description value="Verify that the Secure URL configuration applies to the Downloadable pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15616"/> + <group value="downloadable"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/downloadable/customer" stepKey="goToUnsecureDownloadableCustomerURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/downloadable/customer" stepKey="seeSecureDownloadableCustomerURL"/> + <amOnUrl url="http://{$hostname}/downloadable/download" stepKey="goToUnsecureDownloadableDownloadURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/downloadable/download" stepKey="seeSecureDownloadableDownloadURL"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php index ce01b449d3388..fa989c9e94991 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php @@ -8,6 +8,8 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; /** + * Unit tests for \Magento\Downloadable\Controller\Download\LinkSample. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class LinkSampleTest extends \PHPUnit\Framework\TestCase @@ -63,6 +65,11 @@ class LinkSampleTest extends \PHPUnit\Framework\TestCase */ protected $urlInterface; + /** + * @var \Magento\Catalog\Model\Product\SalabilityChecker|\PHPUnit_Framework_MockObject_MockObject + */ + private $salabilityCheckerMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -83,31 +90,39 @@ protected function setUp() ] ); - $this->helperData = $this->createPartialMock(\Magento\Downloadable\Helper\Data::class, [ - 'getIsShareable' - ]); - $this->downloadHelper = $this->createPartialMock(\Magento\Downloadable\Helper\Download::class, [ + $this->helperData = $this->createPartialMock( + \Magento\Downloadable\Helper\Data::class, + ['getIsShareable'] + ); + $this->downloadHelper = $this->createPartialMock( + \Magento\Downloadable\Helper\Download::class, + [ 'setResource', 'getFilename', 'getContentType', 'getFileSize', 'getContentDisposition', 'output' - ]); - $this->product = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [ + ] + ); + $this->product = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + [ '_wakeup', 'load', 'getId', 'getProductUrl', 'getName' - ]); + ] + ); $this->messageManager = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); $this->urlInterface = $this->createMock(\Magento\Framework\UrlInterface::class); - $this->objectManager = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, [ - 'create', - 'get' - ]); + $this->salabilityCheckerMock = $this->createMock(\Magento\Catalog\Model\Product\SalabilityChecker::class); + $this->objectManager = $this->createPartialMock( + \Magento\Framework\ObjectManager\ObjectManager::class, + ['create', 'get'] + ); $this->linkSample = $this->objectManagerHelper->getObject( \Magento\Downloadable\Controller\Download\LinkSample::class, [ @@ -115,11 +130,17 @@ protected function setUp() 'request' => $this->request, 'response' => $this->response, 'messageManager' => $this->messageManager, - 'redirect' => $this->redirect + 'redirect' => $this->redirect, + 'salabilityChecker' => $this->salabilityCheckerMock, ] ); } + /** + * Execute Download link's sample action with Url link. + * + * @return void + */ public function testExecuteLinkTypeUrl() { $linkMock = $this->getMockBuilder(\Magento\Downloadable\Model\Link::class) @@ -134,6 +155,7 @@ public function testExecuteLinkTypeUrl() ->willReturn($linkMock); $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf(); $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id'); + $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); $linkMock->expects($this->once())->method('getSampleType')->willReturn( \Magento\Downloadable\Helper\Download::LINK_TYPE_URL ); @@ -155,6 +177,11 @@ public function testExecuteLinkTypeUrl() $this->assertEquals($this->response, $this->linkSample->execute()); } + /** + * Execute Download link's sample action with File link. + * + * @return void + */ public function testExecuteLinkTypeFile() { $linkMock = $this->getMockBuilder(\Magento\Downloadable\Model\Link::class) @@ -173,6 +200,7 @@ public function testExecuteLinkTypeFile() ->willReturn($linkMock); $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf(); $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id'); + $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); $linkMock->expects($this->any())->method('getSampleType')->willReturn( \Magento\Downloadable\Helper\Download::LINK_TYPE_FILE ); diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php index 2545e15317ebb..5e711b61e6a5a 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php @@ -8,6 +8,8 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; /** + * Unit tests for \Magento\Downloadable\Controller\Download\Sample. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SampleTest extends \PHPUnit\Framework\TestCase @@ -63,6 +65,11 @@ class SampleTest extends \PHPUnit\Framework\TestCase */ protected $urlInterface; + /** + * @var \Magento\Catalog\Model\Product\SalabilityChecker|\PHPUnit_Framework_MockObject_MockObject + */ + private $salabilityCheckerMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -83,31 +90,39 @@ protected function setUp() ] ); - $this->helperData = $this->createPartialMock(\Magento\Downloadable\Helper\Data::class, [ - 'getIsShareable' - ]); - $this->downloadHelper = $this->createPartialMock(\Magento\Downloadable\Helper\Download::class, [ + $this->helperData = $this->createPartialMock( + \Magento\Downloadable\Helper\Data::class, + ['getIsShareable'] + ); + $this->downloadHelper = $this->createPartialMock( + \Magento\Downloadable\Helper\Download::class, + [ 'setResource', 'getFilename', 'getContentType', 'getFileSize', 'getContentDisposition', 'output' - ]); - $this->product = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [ + ] + ); + $this->product = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + [ '_wakeup', 'load', 'getId', 'getProductUrl', 'getName' - ]); + ] + ); $this->messageManager = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); $this->urlInterface = $this->createMock(\Magento\Framework\UrlInterface::class); - $this->objectManager = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, [ - 'create', - 'get' - ]); + $this->salabilityCheckerMock = $this->createMock(\Magento\Catalog\Model\Product\SalabilityChecker::class); + $this->objectManager = $this->createPartialMock( + \Magento\Framework\ObjectManager\ObjectManager::class, + ['create', 'get'] + ); $this->sample = $this->objectManagerHelper->getObject( \Magento\Downloadable\Controller\Download\Sample::class, [ @@ -115,12 +130,18 @@ protected function setUp() 'request' => $this->request, 'response' => $this->response, 'messageManager' => $this->messageManager, - 'redirect' => $this->redirect + 'redirect' => $this->redirect, + 'salabilityChecker' => $this->salabilityCheckerMock, ] ); } - public function testExecuteLinkTypeUrl() + /** + * Execute Download sample action with Sample Url. + * + * @return void + */ + public function testExecuteSampleWithUrlType() { $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class) ->disableOriginalConstructor() @@ -134,6 +155,7 @@ public function testExecuteLinkTypeUrl() ->willReturn($sampleMock); $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf(); $sampleMock->expects($this->once())->method('getId')->willReturn('some_link_id'); + $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); $sampleMock->expects($this->once())->method('getSampleType')->willReturn( \Magento\Downloadable\Helper\Download::LINK_TYPE_URL ); @@ -155,7 +177,12 @@ public function testExecuteLinkTypeUrl() $this->assertEquals($this->response, $this->sample->execute()); } - public function testExecuteLinkTypeFile() + /** + * Execute Download sample action with Sample File. + * + * @return void + */ + public function testExecuteSampleWithFileType() { $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class) ->disableOriginalConstructor() @@ -173,6 +200,7 @@ public function testExecuteLinkTypeFile() ->willReturn($sampleMock); $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf(); $sampleMock->expects($this->once())->method('getId')->willReturn('some_sample_id'); + $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true); $sampleMock->expects($this->any())->method('getSampleType')->willReturn( \Magento\Downloadable\Helper\Download::LINK_TYPE_FILE ); diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php index e23f81607a0c0..bb2477d4df827 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute.php @@ -203,6 +203,7 @@ protected function _getDefaultSourceModel() * Delete entity * * @return \Magento\Eav\Model\ResourceModel\Entity\Attribute + * @throws LocalizedException * @codeCoverageIgnore */ public function deleteEntity() @@ -310,9 +311,10 @@ public function beforeSave() } /** - * @inheritdoc + * Save additional data * - * Save additional data. + * @return $this + * @throws LocalizedException */ public function afterSave() { @@ -320,15 +322,6 @@ public function afterSave() return parent::afterSave(); } - /** - * @inheritdoc - * @since 100.0.7 - */ - public function afterDelete() - { - return parent::afterDelete(); - } - /** * Detect backend storage type using frontend input type * @@ -496,14 +489,9 @@ public function getIdentities() /** * @inheritdoc * @since 100.0.7 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->unsetData('attribute_set_info'); return array_diff( parent::__sleep(), @@ -514,14 +502,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.7 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = ObjectManager::getInstance(); $this->_localeDate = $objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index 9ed4ac5293681..3857118ae67ca 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -13,6 +13,7 @@ /** * Entity/Attribute/Model - attribute abstract + * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -1404,14 +1405,9 @@ public function setExtensionAttributes(\Magento\Eav\Api\Data\AttributeExtensionI /** * @inheritdoc * @since 100.0.7 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return array_diff( parent::__sleep(), [ @@ -1434,14 +1430,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.7 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 5e7226e7a36dd..0e7a46125d872 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -725,14 +725,9 @@ public function getValidAttributeIds($attributeIds) * * @return array * @since 100.0.7 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $properties = parent::__sleep(); $properties = array_diff($properties, ['_storeManager']); return $properties; @@ -743,14 +738,9 @@ public function __sleep() * * @return void * @since 100.0.7 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $this->_storeManager = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Store\Model\StoreManagerInterface::class); diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls index 0299067bd0523..0b174fbc4d84d 100644 --- a/app/code/Magento/EavGraphQl/etc/schema.graphqls +++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls @@ -2,7 +2,7 @@ # See COPYING.txt for license details. type Query { - customAttributeMetadata(attributes: [AttributeInput!]!): CustomAttributeMetadata @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") @doc(description: "The customAttributeMetadata query returns the attribute type, given an attribute code and entity type") @cache(cacheable: false) + customAttributeMetadata(attributes: [AttributeInput!]!): CustomAttributeMetadata @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") @doc(description: "The customAttributeMetadata query returns the attribute type, given an attribute code and entity type") @cache(cacheable: false) } type CustomAttributeMetadata @doc(description: "CustomAttributeMetadata defines an array of attribute_codes and entity_types") { diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php index e5c8dd48c7af3..bbfcce6aa695b 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php @@ -45,8 +45,7 @@ public function __construct(ConverterInterface $fieldTypeConverter, $integerType public function getFieldType(AttributeAdapter $attribute): ?string { if (in_array($attribute->getAttributeCode(), $this->integerTypeAttributes, true) - || (($attribute->isIntegerType() || $attribute->isBooleanType()) - && !$attribute->isUserDefined()) + || ($attribute->isIntegerType() || $attribute->isBooleanType()) ) { return $this->fieldTypeConverter->convert(ConverterInterface::INTERNAL_DATA_TYPE_INT); } diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php index e522d4ae5e070..7ac6588b87866 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php @@ -37,8 +37,9 @@ public function __construct(ConverterInterface $fieldTypeConverter) */ public function getFieldType(AttributeAdapter $attribute): ?string { - if ($attribute->isComplexType() - || (!$attribute->isSearchable() && !$attribute->isAlwaysIndexable() && $attribute->isFilterable()) + if (($attribute->isComplexType() + || (!$attribute->isSearchable() && !$attribute->isAlwaysIndexable() && $attribute->isFilterable())) + && !$attribute->isBooleanType() ) { return $this->fieldTypeConverter->convert(ConverterInterface::INTERNAL_DATA_TYPE_KEYWORD); } diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php index c05e8a441604d..93f4caa10adf9 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php @@ -91,7 +91,7 @@ public function ping() } /** - * Validate connection params + * Validate connection params. * * @return bool */ @@ -109,7 +109,9 @@ public function testConnection() private function buildConfig($options = []) { $host = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); + // @codingStandardsIgnoreStart $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); + // @codingStandardsIgnoreEnd if (!$protocol) { $protocol = 'http'; } @@ -144,10 +146,12 @@ public function bulkQuery($query) */ public function createIndex($index, $settings) { - $this->getClient()->indices()->create([ - 'index' => $index, - 'body' => $settings, - ]); + $this->getClient()->indices()->create( + [ + 'index' => $index, + 'body' => $settings, + ] + ); } /** @@ -250,10 +254,12 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'type' => $entityType, 'body' => [ $entityType => [ - '_all' => $this->prepareFieldInfo([ - 'enabled' => true, - 'type' => 'text', - ]), + '_all' => $this->prepareFieldInfo( + [ + 'enabled' => true, + 'type' => 'text', + ] + ), 'properties' => [], 'dynamic_templates' => [ [ @@ -266,25 +272,28 @@ public function addFieldsMapping(array $fields, $index, $entityType) ], ], ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => $this->prepareFieldInfo([ - 'type' => 'text', - 'index' => false, - ]), - ], - ], [ 'position_mapping' => [ 'match' => 'position_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'int', + 'type' => 'integer', + 'index' => false, ], ], ], + [ + 'string_mapping' => [ + 'match' => '*', + 'match_mapping_type' => 'string', + 'mapping' => $this->prepareFieldInfo( + [ + 'type' => 'text', + 'index' => false, + ] + ), + ], + ] ], ], ], @@ -329,10 +338,9 @@ private function prepareFieldInfo($fieldInfo) */ public function deleteMapping($index, $entityType) { - $this->getClient()->indices()->deleteMapping([ - 'index' => $index, - 'type' => $entityType, - ]); + $this->getClient()->indices()->deleteMapping( + ['index' => $index, 'type' => $entityType] + ); } /** diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php index 09968db00aa25..75c675663f03f 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php @@ -49,27 +49,24 @@ class Builder /** * @var Sort */ - protected $sortBuilder; + private $sortBuilder; /** * @param Config $clientConfig * @param SearchIndexNameResolver $searchIndexNameResolver * @param AggregationBuilder $aggregationBuilder * @param ScopeResolverInterface $scopeResolver - * @param Sort|null $sortBuilder */ public function __construct( Config $clientConfig, SearchIndexNameResolver $searchIndexNameResolver, AggregationBuilder $aggregationBuilder, - ScopeResolverInterface $scopeResolver, - Sort $sortBuilder = null + ScopeResolverInterface $scopeResolver ) { $this->clientConfig = $clientConfig; $this->searchIndexNameResolver = $searchIndexNameResolver; $this->aggregationBuilder = $aggregationBuilder; $this->scopeResolver = $scopeResolver; - $this->sortBuilder = $sortBuilder ?: ObjectManager::getInstance()->get(Sort::class); } /** @@ -91,7 +88,7 @@ public function initQuery(RequestInterface $request) 'from' => $request->getFrom(), 'size' => $request->getSize(), 'stored_fields' => ['_id', '_score'], - 'sort' => $this->sortBuilder->getSort($request), + 'sort' => $this->getSortBuilder()->getSort($request), 'query' => [], ], ]; @@ -112,4 +109,17 @@ public function initAggregations( ) { return $this->aggregationBuilder->build($request, $searchQuery); } + + /** + * Get sort builder instance. + * + * @return Sort + */ + private function getSortBuilder() + { + if (null === $this->sortBuilder) { + $this->sortBuilder = ObjectManager::getInstance()->get(Sort::class); + } + return $this->sortBuilder; + } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php index 54586fa357ff0..165f7e78eb65f 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php @@ -159,9 +159,9 @@ public function isSortable(): bool /** * Check if attribute is defined by user. * - * @return string + * @return bool|null */ - public function isUserDefined(): string + public function isUserDefined() { return $this->getAttribute()->getIsUserDefined(); } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php index 268fe00e4c41e..76bc7a15e47a7 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php @@ -125,7 +125,7 @@ public function getFields(array $context = []): array ['categoryId' => $categoryId] ); $allAttributes[$categoryPositionKey] = [ - 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_INT), 'index' => $this->indexTypeConverter->convert(IndexTypeConverterInterface::INTERNAL_NO_INDEX_VALUE) ]; $allAttributes[$categoryNameKey] = [ diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php index 7793f60cd1efc..f1e6c8abeb439 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php @@ -37,9 +37,7 @@ public function __construct(ConverterInterface $fieldTypeConverter) */ public function getFieldType(AttributeAdapter $attribute): ?string { - if (($attribute->isIntegerType() || $attribute->isBooleanType()) - && !$attribute->isUserDefined() - ) { + if ($attribute->isIntegerType() || $attribute->isBooleanType()) { return $this->fieldTypeConverter->convert(ConverterInterface::INTERNAL_DATA_TYPE_INT); } diff --git a/app/code/Magento/Elasticsearch/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Client/Elasticsearch.php index 44ab0dbc4d46c..f9b827304446d 100644 --- a/app/code/Magento/Elasticsearch/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Model/Client/Elasticsearch.php @@ -96,13 +96,17 @@ public function testConnection() } /** + * Build config. + * * @param array $options * @return array */ private function buildConfig($options = []) { $host = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); + // @codingStandardsIgnoreStart $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); + // @codingStandardsIgnoreEnd if (!$protocol) { $protocol = 'http'; } @@ -137,10 +141,12 @@ public function bulkQuery($query) */ public function createIndex($index, $settings) { - $this->getClient()->indices()->create([ - 'index' => $index, - 'body' => $settings, - ]); + $this->getClient()->indices()->create( + [ + 'index' => $index, + 'body' => $settings, + ] + ); } /** @@ -202,9 +208,10 @@ public function indexExists($index) } /** + * Check if alias exists. + * * @param string $alias * @param string $index - * * @return bool */ public function existsAlias($alias, $index = '') @@ -217,8 +224,9 @@ public function existsAlias($alias, $index = '') } /** - * @param string $alias + * Get alias. * + * @param string $alias * @return array */ public function getAlias($alias) @@ -252,29 +260,31 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match' => 'price_*', 'match_mapping' => 'string', 'mapping' => [ - 'type' => 'float' + 'type' => 'float', + 'store' => true ], ], ], [ - 'string_mapping' => [ - 'match' => '*', + 'position_mapping' => [ + 'match' => 'position_*', 'match_mapping' => 'string', 'mapping' => [ - 'type' => 'string', + 'type' => 'integer', 'index' => 'no' ], ], ], [ - 'position_mapping' => [ - 'match' => 'position_*', + 'string_mapping' => [ + 'match' => '*', 'match_mapping' => 'string', 'mapping' => [ - 'type' => 'int' + 'type' => 'string', + 'index' => 'no' ], ], - ], + ] ], ], ], @@ -294,10 +304,12 @@ public function addFieldsMapping(array $fields, $index, $entityType) */ public function deleteMapping($index, $entityType) { - $this->getClient()->indices()->deleteMapping([ - 'index' => $index, - 'type' => $entityType, - ]); + $this->getClient()->indices()->deleteMapping( + [ + 'index' => $index, + 'type' => $entityType, + ] + ); } /** diff --git a/app/code/Magento/Elasticsearch/Model/Config.php b/app/code/Magento/Elasticsearch/Model/Config.php index 387db07c62f90..0bf23f318c3bd 100644 --- a/app/code/Magento/Elasticsearch/Model/Config.php +++ b/app/code/Magento/Elasticsearch/Model/Config.php @@ -106,7 +106,15 @@ public function prepareClientOptions($options = []) 'timeout' => $this->getElasticsearchConfigData('server_timeout') ? : self::ELASTICSEARCH_DEFAULT_TIMEOUT, ]; $options = array_merge($defaultOptions, $options); - return $options; + $allowedOptions = array_merge(array_keys($defaultOptions), ['engine']); + + return array_filter( + $options, + function (string $key) use ($allowedOptions) { + return in_array($key, $allowedOptions); + }, + ARRAY_FILTER_USE_KEY + ); } /** diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php b/app/code/Magento/Elasticsearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php deleted file mode 100644 index ec18b955a2917..0000000000000 --- a/app/code/Magento/Elasticsearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php +++ /dev/null @@ -1,94 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Model\Indexer\Plugin; - -use Magento\Elasticsearch\Model\Config; -use Magento\CatalogInventory\Api\StockConfigurationInterface; -use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; -use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; -use Magento\CatalogInventory\Api\Data\StockStatusInterface; -use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; - -/** - * Plugin for filtering child products that are out of stock for preventing their saving to catalog search index. - */ -class StockedProductsFilterPlugin -{ - /** - * @var Config - */ - private $config; - - /** - * @var StockConfigurationInterface - */ - private $stockConfiguration; - - /** - * @var StockStatusRepositoryInterface - */ - private $stockStatusRepository; - - /** - * @var StockStatusCriteriaInterfaceFactory - */ - private $stockStatusCriteriaFactory; - - /** - * @param Config $config - * @param StockConfigurationInterface $stockConfiguration - * @param StockStatusRepositoryInterface $stockStatusRepository - * @param StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory - */ - public function __construct( - Config $config, - StockConfigurationInterface $stockConfiguration, - StockStatusRepositoryInterface $stockStatusRepository, - StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory - ) { - $this->config = $config; - $this->stockConfiguration = $stockConfiguration; - $this->stockStatusRepository = $stockStatusRepository; - $this->stockStatusCriteriaFactory = $stockStatusCriteriaFactory; - } - - /** - * Filter out of stock options for configurable product. - * - * @param DataProvider $dataProvider - * @param array $indexData - * @param array $productData - * @param int $storeId - * @return array - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforePrepareProductIndex( - DataProvider $dataProvider, - array $indexData, - array $productData, - int $storeId - ): array { - if ($this->config->isElasticsearchEnabled() && !$this->stockConfiguration->isShowOutOfStock($storeId)) { - $productIds = array_keys($indexData); - $stockStatusCriteria = $this->stockStatusCriteriaFactory->create(); - $stockStatusCriteria->setProductsFilter($productIds); - $stockStatusCollection = $this->stockStatusRepository->getList($stockStatusCriteria); - $stockStatuses = $stockStatusCollection->getItems(); - $stockStatuses = array_filter($stockStatuses, function (StockStatusInterface $stockStatus) { - return StockStatusInterface::STATUS_IN_STOCK == $stockStatus->getStockStatus(); - }); - $indexData = array_intersect_key($indexData, $stockStatuses); - } - - return [ - $indexData, - $productData, - $storeId, - ]; - } -} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php new file mode 100644 index 0000000000000..21ff9a53e4f96 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyCheckerInterface; + +/** + * This class add in backward compatibility purposes to check if need to apply old strategy for filter prepare process. + * @deprecated + */ +class DefaultFilterStrategyApplyChecker implements DefaultFilterStrategyApplyCheckerInterface +{ + /** + * Check if this strategy applicable for current engine. + * + * @return bool + */ + public function isApplicable(): bool + { + return false; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php index 32cb85ff750c4..255c7885e84b9 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php @@ -79,7 +79,7 @@ public function resolve(): SearchCriteria $this->builder->setPageSize($this->size); $searchCriteria = $this->builder->create(); $searchCriteria->setRequestName($this->searchRequestName); - $searchCriteria->setSortOrders(array_merge(['relevance' => 'DESC'], $this->orders)); + $searchCriteria->setSortOrders($this->orders); $searchCriteria->setCurrentPage($this->currentPage - 1); return $searchCriteria; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php index d0aaa4b3dd572..0bea8683692f2 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php @@ -6,6 +6,8 @@ namespace Magento\Elasticsearch\SearchAdapter\Query; +use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\RequestInterface; use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query\Builder as Elasticsearch5Builder; @@ -18,7 +20,12 @@ class Builder extends Elasticsearch5Builder { /** - * Set initial settings for query + * @var Sort + */ + private $sortBuilder; + + /** + * Set initial settings for query. * * @param RequestInterface $request * @return array @@ -35,10 +42,23 @@ public function initQuery(RequestInterface $request) 'from' => $request->getFrom(), 'size' => $request->getSize(), 'fields' => ['_id', '_score'], - 'sort' => $this->sortBuilder->getSort($request), + 'sort' => $this->getSortBuilder()->getSort($request), 'query' => [], ], ]; return $searchQuery; } + + /** + * Get sort builder instance. + * + * @return Sort + */ + private function getSortBuilder() + { + if (null === $this->sortBuilder) { + $this->sortBuilder = ObjectManager::getInstance()->get(Sort::class); + } + return $this->sortBuilder; + } } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php index 5ccf202e3812b..e8085787f2b44 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php @@ -78,6 +78,13 @@ public function __construct( public function getSort(RequestInterface $request) { $sorts = []; + /** + * Temporary solution for an existing interface of a fulltext search request in Backward compatibility purposes. + * Scope to split Search request interface on two different 'Search' and 'Fulltext Search' contains in MC-16461. + */ + if (!method_exists($request, 'getSort')) { + return $sorts; + } foreach ($request->getSort() as $item) { if (in_array($item['field'], $this->skippedFields)) { continue; diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php index 39155e4f4fe02..c5eea2f897ea6 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php @@ -53,15 +53,18 @@ protected function setUp() /** * @dataProvider getFieldTypeProvider - * @param $attributeCode - * @param $isIntegerType - * @param $isBooleanType - * @param $isUserDefined - * @param $expected + * @param string $attributeCode + * @param bool $isIntegerType + * @param bool $isBooleanType + * @param string $expected * @return void */ - public function testGetFieldType($attributeCode, $isIntegerType, $isBooleanType, $isUserDefined, $expected) - { + public function testGetFieldType( + string $attributeCode, + bool $isIntegerType, + bool $isBooleanType, + string $expected + ): void { $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() ->setMethods(['getAttributeCode', 'isIntegerType', 'isBooleanType', 'isUserDefined']) @@ -75,9 +78,6 @@ public function testGetFieldType($attributeCode, $isIntegerType, $isBooleanType, $attributeMock->expects($this->any()) ->method('isBooleanType') ->willReturn($isBooleanType); - $attributeMock->expects($this->any()) - ->method('isUserDefined') - ->willReturn($isUserDefined); $this->fieldTypeConverter->expects($this->any()) ->method('convert') ->willReturn('something'); @@ -94,12 +94,12 @@ public function testGetFieldType($attributeCode, $isIntegerType, $isBooleanType, public function getFieldTypeProvider() { return [ - ['category_ids', true, true, true, 'something'], - ['category_ids', false, false, false, 'something'], - ['type', true, false, false, 'something'], - ['type', false, true, false, 'something'], - ['type', true, true, true, ''], - ['type', false, false, true, ''], + ['category_ids', true, true, 'something'], + ['category_ids', false, false, 'something'], + ['type', true, false, 'something'], + ['type', false, true, 'something'], + ['type', true, true, 'something'], + ['type', false, false, ''], ]; } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php index 0e33498a16fba..92d523e6c2346 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php @@ -53,18 +53,25 @@ protected function setUp() /** * @dataProvider getFieldTypeProvider - * @param $isComplexType - * @param $isSearchable - * @param $isAlwaysIndexable - * @param $isFilterable - * @param $expected + * @param bool $isComplexType + * @param bool $isSearchable + * @param bool $isAlwaysIndexable + * @param bool $isFilterable + * @param bool $isBoolean + * @param string $expected * @return void */ - public function testGetFieldType($isComplexType, $isSearchable, $isAlwaysIndexable, $isFilterable, $expected) - { + public function testGetFieldType( + bool $isComplexType, + bool $isSearchable, + bool $isAlwaysIndexable, + bool $isFilterable, + bool $isBoolean, + string $expected + ): void { $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['isComplexType', 'isSearchable', 'isAlwaysIndexable', 'isFilterable']) + ->setMethods(['isComplexType', 'isSearchable', 'isAlwaysIndexable', 'isFilterable', 'isBooleanType']) ->getMock(); $attributeMock->expects($this->any()) ->method('isComplexType') @@ -78,6 +85,9 @@ public function testGetFieldType($isComplexType, $isSearchable, $isAlwaysIndexab $attributeMock->expects($this->any()) ->method('isFilterable') ->willReturn($isFilterable); + $attributeMock->expects($this->any()) + ->method('isBooleanType') + ->willReturn($isBoolean); $this->fieldTypeConverter->expects($this->any()) ->method('convert') ->willReturn('something'); @@ -94,13 +104,14 @@ public function testGetFieldType($isComplexType, $isSearchable, $isAlwaysIndexab public function getFieldTypeProvider() { return [ - [true, true, true, true, 'something'], - [true, false, false, false, 'something'], - [true, false, false, true, 'something'], - [false, false, false, true, 'something'], - [false, false, false, false, ''], - [false, false, true, false, ''], - [false, true, false, false, ''], + [true, true, true, true, false, 'something'], + [true, false, false, false, false, 'something'], + [true, false, false, true, false, 'something'], + [false, false, false, true, false, 'something'], + [false, false, false, false, false, ''], + [false, false, true, false, false, ''], + [false, true, false, false, false, ''], + [true, true, true, true, true, ''], ]; } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php index 8fbd183441b6d..5f5807e212961 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php @@ -8,6 +8,9 @@ use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +/** + * Class ElasticsearchTest + */ class ElasticsearchTest extends \PHPUnit\Framework\TestCase { /** @@ -38,30 +41,34 @@ class ElasticsearchTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->elasticsearchClientMock = $this->getMockBuilder(\Elasticsearch\Client::class) - ->setMethods([ - 'indices', - 'ping', - 'bulk', - 'search', - 'scroll', - 'suggest', - 'info', - ]) + ->setMethods( + [ + 'indices', + 'ping', + 'bulk', + 'search', + 'scroll', + 'suggest', + 'info', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->indicesMock = $this->getMockBuilder(\Elasticsearch\Namespaces\IndicesNamespace::class) - ->setMethods([ - 'exists', - 'getSettings', - 'create', - 'delete', - 'putMapping', - 'deleteMapping', - 'stats', - 'updateAliases', - 'existsAlias', - 'getAlias', - ]) + ->setMethods( + [ + 'exists', + 'getSettings', + 'create', + 'delete', + 'putMapping', + 'deleteMapping', + 'stats', + 'updateAliases', + 'existsAlias', + 'getAlias', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->elasticsearchClientMock->expects($this->any()) @@ -174,10 +181,12 @@ public function testCreateIndexExists() { $this->indicesMock->expects($this->once()) ->method('create') - ->with([ - 'index' => 'indexName', - 'body' => [], - ]); + ->with( + [ + 'index' => 'indexName', + 'body' => [], + ] + ); $this->model->createIndex('indexName', []); } @@ -263,9 +272,7 @@ public function testIndexExists() { $this->indicesMock->expects($this->once()) ->method('exists') - ->with([ - 'index' => 'indexName', - ]) + ->with(['index' => 'indexName']) ->willReturn(true); $this->model->indexExists('indexName'); } @@ -321,10 +328,12 @@ public function testCreateIndexFailure() { $this->indicesMock->expects($this->once()) ->method('create') - ->with([ - 'index' => 'indexName', - 'body' => [], - ]) + ->with( + [ + 'index' => 'indexName', + 'body' => [], + ] + ) ->willThrowException(new \Exception('Something went wrong')); $this->model->createIndex('indexName', []); } @@ -336,54 +345,57 @@ public function testAddFieldsMapping() { $this->indicesMock->expects($this->once()) ->method('putMapping') - ->with([ - 'index' => 'indexName', - 'type' => 'product', - 'body' => [ - 'product' => [ - '_all' => [ - 'enabled' => true, - 'type' => 'text', - ], - 'properties' => [ - 'name' => [ + ->with( + [ + 'index' => 'indexName', + 'type' => 'product', + 'body' => [ + 'product' => [ + '_all' => [ + 'enabled' => true, 'type' => 'text', ], - ], - 'dynamic_templates' => [ - [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'float', - 'store' => true, - ], + 'properties' => [ + 'name' => [ + 'type' => 'text', ], ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'text', - 'index' => false, + 'dynamic_templates' => [ + [ + 'price_mapping' => [ + 'match' => 'price_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'float', + 'store' => true, + ], ], ], - ], - [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'int', + [ + 'position_mapping' => [ + 'match' => 'position_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'integer', + 'index' => false + ], + ], + ], + [ + 'string_mapping' => [ + 'match' => '*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'text', + 'index' => false, + ], ], ], ], ], ], - ], - ]); + ] + ); $this->model->addFieldsMapping( [ 'name' => [ @@ -403,54 +415,57 @@ public function testAddFieldsMappingFailure() { $this->indicesMock->expects($this->once()) ->method('putMapping') - ->with([ - 'index' => 'indexName', - 'type' => 'product', - 'body' => [ - 'product' => [ - '_all' => [ - 'enabled' => true, - 'type' => 'text', - ], - 'properties' => [ - 'name' => [ + ->with( + [ + 'index' => 'indexName', + 'type' => 'product', + 'body' => [ + 'product' => [ + '_all' => [ + 'enabled' => true, 'type' => 'text', ], - ], - 'dynamic_templates' => [ - [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'float', - 'store' => true, - ], + 'properties' => [ + 'name' => [ + 'type' => 'text', ], ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'text', - 'index' => false, + 'dynamic_templates' => [ + [ + 'price_mapping' => [ + 'match' => 'price_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'float', + 'store' => true, + ], ], ], - ], - [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'int', + [ + 'position_mapping' => [ + 'match' => 'position_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'integer', + 'index' => false + ], ], ], + [ + 'string_mapping' => [ + 'match' => '*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'text', + 'index' => false, + ], + ], + ] ], ], ], - ], - ]) + ] + ) ->willThrowException(new \Exception('Something went wrong')); $this->model->addFieldsMapping( [ @@ -470,10 +485,12 @@ public function testDeleteMapping() { $this->indicesMock->expects($this->once()) ->method('deleteMapping') - ->with([ - 'index' => 'indexName', - 'type' => 'product', - ]); + ->with( + [ + 'index' => 'indexName', + 'type' => 'product', + ] + ); $this->model->deleteMapping( 'indexName', 'product' @@ -488,10 +505,12 @@ public function testDeleteMappingFailure() { $this->indicesMock->expects($this->once()) ->method('deleteMapping') - ->with([ - 'index' => 'indexName', - 'type' => 'product', - ]) + ->with( + [ + 'index' => 'indexName', + 'type' => 'product', + ] + ) ->willThrowException(new \Exception('Something went wrong')); $this->model->deleteMapping( 'indexName', diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php index 7c2a33c05aa08..cdb4ea4021f79 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -184,21 +184,25 @@ public function testGetAllAttributesTypes( $this->fieldNameResolver->expects($this->any()) ->method('getFieldName') - ->will($this->returnCallback( - function ($attribute) use ($categoryId) { - static $callCount = []; - $attributeCode = $attribute->getAttributeCode(); - $callCount[$attributeCode] = !isset($callCount[$attributeCode]) ? 1 : ++$callCount[$attributeCode]; + ->will( + $this->returnCallback( + function ($attribute) use ($categoryId) { + static $callCount = []; + $attributeCode = $attribute->getAttributeCode(); + $callCount[$attributeCode] = !isset($callCount[$attributeCode]) + ? 1 + : ++$callCount[$attributeCode]; - if ($attributeCode === 'category') { - return 'category_name_' . $categoryId; - } elseif ($attributeCode === 'position') { - return 'position_' . $categoryId; - } elseif ($attributeCode === 'price') { - return 'price_' . $categoryId . '_1'; + if ($attributeCode === 'category') { + return 'category_name_' . $categoryId; + } elseif ($attributeCode === 'position') { + return 'position_' . $categoryId; + } elseif ($attributeCode === 'price') { + return 'price_' . $categoryId . '_1'; + } } - } - )); + ) + ); $priceAttributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() ->setMethods(['getAttributeCode']) @@ -215,44 +219,47 @@ function ($attribute) use ($categoryId) { $this->attributeAdapterProvider->expects($this->any()) ->method('getByAttributeCode') ->with($this->anything()) - ->will($this->returnCallback( - function ($code) use ( - $categoryAttributeMock, - $positionAttributeMock, - $priceAttributeMock - ) { - static $callCount = []; - $callCount[$code] = !isset($callCount[$code]) ? 1 : ++$callCount[$code]; + ->will( + $this->returnCallback( + function ($code) use ( + $categoryAttributeMock, + $positionAttributeMock, + $priceAttributeMock + ) { + static $callCount = []; + $callCount[$code] = !isset($callCount[$code]) ? 1 : ++$callCount[$code]; - if ($code === 'position') { - return $positionAttributeMock; - } elseif ($code === 'category_name') { - return $categoryAttributeMock; - } elseif ($code === 'price') { - return $priceAttributeMock; + if ($code === 'position') { + return $positionAttributeMock; + } elseif ($code === 'category_name') { + return $categoryAttributeMock; + } elseif ($code === 'price') { + return $priceAttributeMock; + } } - } - )); + ) + ); $this->fieldTypeConverter->expects($this->any()) ->method('convert') ->with($this->anything()) - ->will($this->returnCallback( - function ($type) use ($complexType) { - static $callCount = []; - $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; + ->will( + $this->returnCallback( + function ($type) use ($complexType) { + static $callCount = []; + $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; - if ($type === 'string') { - return 'string'; + if ($type === 'string') { + return 'string'; + } elseif ($type === 'float') { + return 'float'; + } elseif ($type === 'integer') { + return 'integer'; + } else { + return $complexType; + } } - if ($type === 'string') { - return 'string'; - } elseif ($type === 'float') { - return 'float'; - } else { - return $complexType; - } - } - )); + ) + ); $this->assertEquals( $expected, @@ -276,7 +283,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'position_1' => [ - 'type' => 'string', + 'type' => 'integer', 'index' => 'no_index' ], 'price_1_1' => [ @@ -295,7 +302,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'position_1' => [ - 'type' => 'string', + 'type' => 'integer', 'index' => 'no_index' ], 'price_1_1' => [ @@ -314,7 +321,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'position_1' => [ - 'type' => 'string', + 'type' => 'integer', 'index' => 'no_index' ], 'price_1_1' => [ diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php index 7570c8971b27b..c45ebe20b6be6 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php @@ -52,15 +52,18 @@ protected function setUp() /** * @dataProvider getFieldTypeProvider - * @param $attributeCode - * @param $isIntegerType - * @param $isBooleanType - * @param $isUserDefined - * @param $expected + * @param string $attributeCode + * @param bool $isIntegerType + * @param bool $isBooleanType + * @param string|null $expected * @return void */ - public function testGetFieldType($attributeCode, $isIntegerType, $isBooleanType, $isUserDefined, $expected) - { + public function testGetFieldType( + string $attributeCode, + bool $isIntegerType, + bool $isBooleanType, + $expected + ): void { $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() ->setMethods(['getAttributeCode', 'isIntegerType', 'isBooleanType', 'isUserDefined']) @@ -74,9 +77,6 @@ public function testGetFieldType($attributeCode, $isIntegerType, $isBooleanType, $attributeMock->expects($this->any()) ->method('isBooleanType') ->willReturn($isBooleanType); - $attributeMock->expects($this->any()) - ->method('isUserDefined') - ->willReturn($isUserDefined); $this->fieldTypeConverter->expects($this->any()) ->method('convert') ->willReturn('something'); @@ -93,12 +93,12 @@ public function testGetFieldType($attributeCode, $isIntegerType, $isBooleanType, public function getFieldTypeProvider() { return [ - ['category_ids', true, true, true, null], - ['category_ids', false, false, false, null], - ['type', true, false, false, 'something'], - ['type', false, true, false, 'something'], - ['type', true, true, true, ''], - ['type', false, false, true, ''], + ['category_ids', true, true, 'something'], + ['category_ids', false, false, null], + ['type', true, false, 'something'], + ['type', false, true, 'something'], + ['type', true, true, 'something'], + ['type', false, false, ''], ]; } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/ConfigTest.php index 3829a2f9280c1..dfe6deb23c22f 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/ConfigTest.php @@ -61,7 +61,7 @@ public function testPrepareClientOptions() 'password' => 'pass', 'timeout' => 1, ]; - $this->assertEquals($options, $this->model->prepareClientOptions($options)); + $this->assertEquals($options, $this->model->prepareClientOptions(array_merge($options, ['test' => 'test']))); } /** diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php deleted file mode 100644 index f66d2532b32ae..0000000000000 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php +++ /dev/null @@ -1,134 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Test\Unit\Model\Indexer\Plugin; - -use Magento\Elasticsearch\Model\Config; -use Magento\Elasticsearch\Model\Indexer\Plugin\StockedProductsFilterPlugin; -use Magento\CatalogInventory\Api\StockConfigurationInterface; -use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; -use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; -use Magento\CatalogInventory\Api\StockStatusCriteriaInterface; -use Magento\CatalogInventory\Api\Data\StockStatusCollectionInterface; -use Magento\CatalogInventory\Api\Data\StockStatusInterface; -use Magento\CatalogInventory\Model\Stock; -use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; - -/** - * Test for Magento\Elasticsearch\Model\Indexer\Plugin\StockedProductsFilterPlugin class. - */ -class StockedProductsFilterPluginTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject - */ - private $configMock; - - /** - * @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $stockConfigurationMock; - - /** - * @var StockStatusRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $stockStatusRepositoryMock; - - /** - * @var StockStatusCriteriaInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject - */ - private $stockStatusCriteriaFactoryMock; - - /** - * @var StockedProductsFilterPlugin - */ - private $plugin; - - /** - * {@inheritdoc} - */ - protected function setUp() - { - $this->configMock = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); - $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->stockStatusRepositoryMock = $this->getMockBuilder(StockStatusRepositoryInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->stockStatusCriteriaFactoryMock = $this->getMockBuilder(StockStatusCriteriaInterfaceFactory::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->plugin = new StockedProductsFilterPlugin( - $this->configMock, - $this->stockConfigurationMock, - $this->stockStatusRepositoryMock, - $this->stockStatusCriteriaFactoryMock - ); - } - - /** - * @return void - */ - public function testBeforePrepareProductIndex(): void - { - /** @var DataProvider|\PHPUnit_Framework_MockObject_MockObject $dataProviderMock */ - $dataProviderMock = $this->getMockBuilder(DataProvider::class)->disableOriginalConstructor()->getMock(); - $indexData = [ - 1 => [], - 2 => [], - ]; - $productData = []; - $storeId = 1; - - $this->configMock - ->expects($this->once()) - ->method('isElasticsearchEnabled') - ->willReturn(true); - $this->stockConfigurationMock - ->expects($this->once()) - ->method('isShowOutOfStock') - ->willReturn(false); - - $stockStatusCriteriaMock = $this->getMockBuilder(StockStatusCriteriaInterface::class)->getMock(); - $stockStatusCriteriaMock - ->expects($this->once()) - ->method('setProductsFilter') - ->willReturn(true); - $this->stockStatusCriteriaFactoryMock - ->expects($this->once()) - ->method('create') - ->willReturn($stockStatusCriteriaMock); - - $stockStatusMock = $this->getMockBuilder(StockStatusInterface::class)->getMock(); - $stockStatusMock->expects($this->atLeastOnce()) - ->method('getStockStatus') - ->willReturnOnConsecutiveCalls(Stock::STOCK_IN_STOCK, Stock::STOCK_OUT_OF_STOCK); - $stockStatusCollectionMock = $this->getMockBuilder(StockStatusCollectionInterface::class)->getMock(); - $stockStatusCollectionMock - ->expects($this->once()) - ->method('getItems') - ->willReturn([ - 1 => $stockStatusMock, - 2 => $stockStatusMock, - ]); - $this->stockStatusRepositoryMock - ->expects($this->once()) - ->method('getList') - ->willReturn($stockStatusCollectionMock); - - list ($indexData, $productData, $storeId) = $this->plugin->beforePrepareProductIndex( - $dataProviderMock, - $indexData, - $productData, - $storeId - ); - - $this->assertEquals([1], array_keys($indexData)); - } -} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php new file mode 100644 index 0000000000000..30a1642378b71 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\ResourceModel\Fulltext\Collection; + +use Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolver; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchCriteria; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * Unit test for SearchCriteriaResolver + */ +class SearchCriteriaResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $searchCriteriaBuilder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->searchCriteriaBuilder = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->disableOriginalConstructor() + ->setMethods(['setPageSize', 'create']) + ->getMock(); + } + + /** + * @param array|null $orders + * @param array|null $expected + * @dataProvider resolveSortOrderDataProvider + */ + public function testResolve($orders, $expected) + { + $searchRequestName = 'test'; + $currentPage = 1; + $size = 10; + + $searchCriteria = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor() + ->setMethods(['setRequestName', 'setSortOrders', 'setCurrentPage']) + ->getMock(); + $searchCriteria->expects($this->once()) + ->method('setRequestName') + ->with($searchRequestName) + ->willReturn($searchCriteria); + $searchCriteria->expects($this->once()) + ->method('setSortOrders') + ->with($expected) + ->willReturn($searchCriteria); + $searchCriteria->expects($this->once()) + ->method('setCurrentPage') + ->with($currentPage - 1) + ->willReturn($searchCriteria); + + $this->searchCriteriaBuilder->expects($this->once()) + ->method('create') + ->willReturn($searchCriteria); + $this->searchCriteriaBuilder->expects($this->once()) + ->method('setPageSize') + ->with($size) + ->willReturn($this->searchCriteriaBuilder); + + $objectManager = new ObjectManagerHelper($this); + /** @var SearchCriteriaResolver $model */ + $model = $objectManager->getObject( + SearchCriteriaResolver::class, + [ + 'builder' => $this->searchCriteriaBuilder, + 'searchRequestName' => $searchRequestName, + 'currentPage' => $currentPage, + 'size' => $size, + 'orders' => $orders, + ] + ); + + $model->resolve(); + } + + /** + * @return array + */ + public function resolveSortOrderDataProvider() + { + return [ + [ + null, + null, + ], + [ + ['test' => 'ASC'], + ['test' => 'ASC'], + ], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 9732ae8226431..55df6a5a37f46 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -49,6 +49,7 @@ <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + <argument name="defaultFilterStrategyApplyChecker" xsi:type="object">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyChecker</argument> </arguments> </virtualType> <virtualType name="elasticsearchFulltextSearchCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory"> @@ -71,6 +72,7 @@ <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + <argument name="defaultFilterStrategyApplyChecker" xsi:type="object">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyChecker</argument> </arguments> </virtualType> <virtualType name="elasticsearchCategoryCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory"> @@ -93,6 +95,7 @@ <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + <argument name="defaultFilterStrategyApplyChecker" xsi:type="object">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyChecker</argument> </arguments> </virtualType> <virtualType name="elasticsearchAdvancedCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory"> @@ -313,9 +316,6 @@ </argument> </arguments> </type> - <type name="Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider"> - <plugin name="stockedProductsFilterPlugin" type="Magento\Elasticsearch\Model\Indexer\Plugin\StockedProductsFilterPlugin"/> - </type> <type name="Magento\Framework\Indexer\Config\DependencyInfoProvider"> <plugin name="indexerDependencyUpdaterPlugin" type="Magento\Elasticsearch\Model\Indexer\Plugin\DependencyUpdaterPlugin"/> </type> @@ -478,7 +478,7 @@ <argument name="indexTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> <argument name="fieldIndexResolver" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver</argument> <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> - <argument name="fieldNameResolver" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface</argument> + <argument name="fieldNameResolver" xsi:type="object">elasticsearch5FieldNameResolver</argument> </arguments> </virtualType> <virtualType name="elasticsearch5DynamicFieldProvider" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\DynamicField"> diff --git a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php index af39b24acda56..34129a5af0012 100644 --- a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php @@ -104,7 +104,9 @@ public function testConnection() private function buildConfig($options = []) { $host = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); + // @codingStandardsIgnoreStart $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); + // @codingStandardsIgnoreEnd if (!$protocol) { $protocol = 'http'; } @@ -139,10 +141,12 @@ public function bulkQuery($query) */ public function createIndex($index, $settings) { - $this->getClient()->indices()->create([ - 'index' => $index, - 'body' => $settings, - ]); + $this->getClient()->indices()->create( + [ + 'index' => $index, + 'body' => $settings, + ] + ); } /** @@ -262,22 +266,23 @@ public function addFieldsMapping(array $fields, $index, $entityType) ], ], [ - 'string_mapping' => [ - 'match' => '*', + 'position_mapping' => [ + 'match' => 'position_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'text', + 'type' => 'integer', 'index' => false, - 'copy_to' => '_search' ], ], ], [ - 'position_mapping' => [ - 'match' => 'position_*', + 'string_mapping' => [ + 'match' => '*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'int', + 'type' => 'text', + 'index' => false, + 'copy_to' => '_search' ], ], ], @@ -302,10 +307,12 @@ public function addFieldsMapping(array $fields, $index, $entityType) */ public function deleteMapping($index, $entityType) { - $this->getClient()->indices()->deleteMapping([ - 'index' => $index, - 'type' => $entityType, - ]); + $this->getClient()->indices()->deleteMapping( + [ + 'index' => $index, + 'type' => $entityType, + ] + ); } /** diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php index 8276d0dd8dbe8..487a5a886f951 100644 --- a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php @@ -8,6 +8,9 @@ use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +/** + * Class ElasticsearchTest + */ class ElasticsearchTest extends \PHPUnit\Framework\TestCase { /** @@ -38,30 +41,34 @@ class ElasticsearchTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->elasticsearchClientMock = $this->getMockBuilder(\Elasticsearch\Client::class) - ->setMethods([ - 'indices', - 'ping', - 'bulk', - 'search', - 'scroll', - 'suggest', - 'info', - ]) + ->setMethods( + [ + 'indices', + 'ping', + 'bulk', + 'search', + 'scroll', + 'suggest', + 'info', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->indicesMock = $this->getMockBuilder(\Elasticsearch\Namespaces\IndicesNamespace::class) - ->setMethods([ - 'exists', - 'getSettings', - 'create', - 'delete', - 'putMapping', - 'deleteMapping', - 'stats', - 'updateAliases', - 'existsAlias', - 'getAlias', - ]) + ->setMethods( + [ + 'exists', + 'getSettings', + 'create', + 'delete', + 'putMapping', + 'deleteMapping', + 'stats', + 'updateAliases', + 'existsAlias', + 'getAlias', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->elasticsearchClientMock->expects($this->any()) @@ -174,10 +181,12 @@ public function testCreateIndexExists() { $this->indicesMock->expects($this->once()) ->method('create') - ->with([ - 'index' => 'indexName', - 'body' => [], - ]); + ->with( + [ + 'index' => 'indexName', + 'body' => [], + ] + ); $this->model->createIndex('indexName', []); } @@ -263,9 +272,7 @@ public function testIndexExists() { $this->indicesMock->expects($this->once()) ->method('exists') - ->with([ - 'index' => 'indexName', - ]) + ->with(['index' => 'indexName']) ->willReturn(true); $this->model->indexExists('indexName'); } @@ -321,10 +328,12 @@ public function testCreateIndexFailure() { $this->indicesMock->expects($this->once()) ->method('create') - ->with([ - 'index' => 'indexName', - 'body' => [], - ]) + ->with( + [ + 'index' => 'indexName', + 'body' => [], + ] + ) ->willThrowException(new \Exception('Something went wrong')); $this->model->createIndex('indexName', []); } @@ -336,54 +345,57 @@ public function testAddFieldsMapping() { $this->indicesMock->expects($this->once()) ->method('putMapping') - ->with([ - 'index' => 'indexName', - 'type' => 'product', - 'body' => [ - 'product' => [ - 'properties' => [ - '_search' => [ - 'type' => 'text', - ], - 'name' => [ - 'type' => 'text', - ], - ], - 'dynamic_templates' => [ - [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'float', - 'store' => true, - ], + ->with( + [ + 'index' => 'indexName', + 'type' => 'product', + 'body' => [ + 'product' => [ + 'properties' => [ + '_search' => [ + 'type' => 'text', + ], + 'name' => [ + 'type' => 'text', ], ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'text', - 'index' => false, - 'copy_to' => '_search' + 'dynamic_templates' => [ + [ + 'price_mapping' => [ + 'match' => 'price_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'float', + 'store' => true, + ], ], ], - ], - [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'int', + [ + 'position_mapping' => [ + 'match' => 'position_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'integer', + 'index' => false + ], ], ], + [ + 'string_mapping' => [ + 'match' => '*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'text', + 'index' => false, + 'copy_to' => '_search' + ], + ], + ] ], ], ], - ], - ]); + ] + ); $this->model->addFieldsMapping( [ 'name' => [ @@ -403,54 +415,57 @@ public function testAddFieldsMappingFailure() { $this->indicesMock->expects($this->once()) ->method('putMapping') - ->with([ - 'index' => 'indexName', - 'type' => 'product', - 'body' => [ - 'product' => [ - 'properties' => [ - '_search' => [ - 'type' => 'text', - ], - 'name' => [ - 'type' => 'text', - ], - ], - 'dynamic_templates' => [ - [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'float', - 'store' => true, - ], + ->with( + [ + 'index' => 'indexName', + 'type' => 'product', + 'body' => [ + 'product' => [ + 'properties' => [ + '_search' => [ + 'type' => 'text', + ], + 'name' => [ + 'type' => 'text', ], ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'text', - 'index' => false, - 'copy_to' => '_search' + 'dynamic_templates' => [ + [ + 'price_mapping' => [ + 'match' => 'price_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'float', + 'store' => true, + ], ], ], - ], - [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'int', + [ + 'position_mapping' => [ + 'match' => 'position_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'integer', + 'index' => false + ], ], ], + [ + 'string_mapping' => [ + 'match' => '*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'text', + 'index' => false, + 'copy_to' => '_search' + ], + ], + ] ], ], ], - ], - ]) + ] + ) ->willThrowException(new \Exception('Something went wrong')); $this->model->addFieldsMapping( [ @@ -470,10 +485,12 @@ public function testDeleteMapping() { $this->indicesMock->expects($this->once()) ->method('deleteMapping') - ->with([ - 'index' => 'indexName', - 'type' => 'product', - ]); + ->with( + [ + 'index' => 'indexName', + 'type' => 'product', + ] + ); $this->model->deleteMapping( 'indexName', 'product' @@ -488,10 +505,12 @@ public function testDeleteMappingFailure() { $this->indicesMock->expects($this->once()) ->method('deleteMapping') - ->with([ - 'index' => 'indexName', - 'type' => 'product', - ]) + ->with( + [ + 'index' => 'indexName', + 'type' => 'product', + ] + ) ->willThrowException(new \Exception('Something went wrong')); $this->model->deleteMapping( 'indexName', diff --git a/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php b/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php index 1ea6e3f921bc6..0ca6615b075b2 100644 --- a/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php +++ b/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php @@ -12,7 +12,7 @@ namespace Magento\Email\Block\Adminhtml\Template; /** - * Template Preview Block + * Email template preview block. * * @api * @since 100.0.2 @@ -70,8 +70,6 @@ protected function _toHtml() $template->setTemplateStyles($this->getRequest()->getParam('styles')); } - $template->setTemplateText($this->_maliciousCode->filter($template->getTemplateText())); - \Magento\Framework\Profiler::start($this->profilerName); $template->emulateDesign($storeId); @@ -80,6 +78,7 @@ protected function _toHtml() [$template, 'getProcessedTemplate'] ); $template->revertDesign(); + $templateProcessed = $this->_maliciousCode->filter($templateProcessed); if ($template->isPlain()) { $templateProcessed = "<pre>" . $this->escapeHtml($templateProcessed) . "</pre>"; diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Popup.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Popup.php new file mode 100644 index 0000000000000..31d172935da7f --- /dev/null +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Popup.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Email\Controller\Adminhtml\Email\Template; + +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Rendering popup email template. + */ +class Popup extends \Magento\Backend\App\Action implements HttpGetActionInterface +{ + /** + * @var \Magento\Framework\View\Result\PageFactory + */ + private $resultPageFactory; + + /** + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, + \Magento\Framework\View\Result\PageFactory $resultPageFactory + ) { + parent::__construct($context); + $this->resultPageFactory = $resultPageFactory; + } + + /** + * Load the page. + * + * Load the page defined in view/adminhtml/layout/adminhtml_email_template_popup.xml + * + * @return \Magento\Framework\View\Result\Page + */ + public function execute() + { + return $this->resultPageFactory->create(); + } + + /** + * @inheritdoc + */ + protected function _isAllowed() + { + return $this->_authorization->isAllowed('Magento_Email::template'); + } +} diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Preview.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Preview.php index 404f97c937167..c1a8eec07e461 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Preview.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Preview.php @@ -6,10 +6,15 @@ */ namespace Magento\Email\Controller\Adminhtml\Email\Template; -class Preview extends \Magento\Email\Controller\Adminhtml\Email\Template +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Rendering email template preview. + */ +class Preview extends \Magento\Email\Controller\Adminhtml\Email\Template implements HttpGetActionInterface { /** - * Preview transactional email action + * Preview transactional email action. * * @return void */ @@ -19,7 +24,7 @@ public function execute() $this->_view->loadLayout(); $this->_view->getPage()->getConfig()->getTitle()->prepend(__('Email Preview')); $this->_view->renderLayout(); - $this->getResponse()->setHeader('Content-Security-Policy', "script-src 'none'"); + $this->getResponse()->setHeader('Content-Security-Policy', "script-src 'self'"); } catch (\Exception $e) { $this->messageManager->addErrorMessage( __('An error occurred. The email template can not be opened for preview.') diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index aa018d6fd44d6..0e27b2d4c418b 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -321,7 +321,7 @@ public function setDesignParams(array $designParams) } /** - * Retrieve CSS processor + * Get CSS processor * * @deprecated 100.1.2 * @return Css\Processor @@ -335,7 +335,7 @@ private function getCssProcessor() } /** - * Retrieve pub directory + * Get pub directory * * @deprecated 100.1.2 * @param string $dirType @@ -855,7 +855,7 @@ public function cssDirective($construction) return $css; } else { // Return CSS comment for debugging purposes - return '/* ' . sprintf(__('Contents of %s could not be loaded or is empty'), $file) . ' */'; + return '/* ' . __('Contents of the specified CSS file could not be loaded or is empty') . ' */'; } } diff --git a/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml b/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml index d299acd28fc1c..4285f6dbdbb08 100644 --- a/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml +++ b/app/code/Magento/Email/Test/Mftf/ActionGroup/EmailTemplateActionGroup.xml @@ -11,29 +11,70 @@ <!--Create New Template --> <actionGroup name="CreateNewTemplate"> + <arguments> + <argument name="template" defaultValue="EmailTemplate"/> + </arguments> + + <!--Go to Marketing> Email Templates--> + <amOnPage url="{{AdminEmailTemplateIndexPage.url}}" stepKey="navigateToEmailTemplatePage"/> <!--Click "Add New Template" button--> - <click stepKey="clickAddNewTemplateButton" selector="{{EmailTemplatesSection.addNewTemplateButton}}"/> - <waitForPageLoad stepKey="waitForNewEmailTemplatesPageLoaded"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickAddNewTemplateButton"/> <!--Select value for "Template" drop-down menu in "Load default template" tab--> - <selectOption selector="{{EmailTemplatesSection.templateDropDown}}" stepKey="selectValueFromTemplateDropDown" userInput="Registry Update"/> - + <selectOption selector="{{AdminEmailTemplateEditSection.templateDropDown}}" userInput="Registry Update" stepKey="selectValueFromTemplateDropDown"/> <!--Fill in required fields in "Template Information" tab and click "Save Template" button--> - <click stepKey="clickLoadTemplateButton" selector="{{EmailTemplatesSection.loadTemplateButton}}" after="selectValueFromTemplateDropDown"/> - <fillField stepKey="fillTemplateNameField" selector="{{EmailTemplatesSection.templateNameField}}" userInput="{{EmailTemplate.templateName}}" after="clickLoadTemplateButton"/> - <waitForLoadingMaskToDisappear stepKey="wait1"/> - <click stepKey="clickSaveTemplateButton" selector="{{EmailTemplatesSection.saveTemplateButton}}"/> - <waitForPageLoad stepKey="waitForNewTemplateCreated"/> + <click selector="{{AdminEmailTemplateEditSection.loadTemplateButton}}" stepKey="clickLoadTemplateButton"/> + <fillField selector="{{AdminEmailTemplateEditSection.templateCode}}" userInput="{{EmailTemplate.templateName}}" stepKey="fillTemplateNameField"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveTemplateButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You saved the email template." stepKey="seeSuccessMessage"/> + </actionGroup> + + <!--Create New Custom Template --> + <actionGroup name="CreateCustomTemplate" extends="CreateNewTemplate"> + <remove keyForRemoval="selectValueFromTemplateDropDown"/> + <remove keyForRemoval="clickLoadTemplateButton"/> + + <fillField selector="{{AdminEmailTemplateEditSection.templateSubject}}" userInput="{{template.templateSubject}}" after="fillTemplateNameField" stepKey="fillTemplateSubject"/> + <fillField selector="{{AdminEmailTemplateEditSection.templateText}}" userInput="{{template.templateText}}" after="fillTemplateSubject" stepKey="fillTemplateText"/> + </actionGroup> + + <!-- Find and Open Email Template --> + <actionGroup name="FindAndOpenEmailTemplate"> + <arguments> + <argument name="template" defaultValue="EmailTemplate"/> + </arguments> + + <amOnPage url="{{AdminEmailTemplateIndexPage.url}}" stepKey="navigateEmailTemplatePage" /> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clearFilters"/> + <fillField selector="{{AdminEmailTemplateIndexSection.searchTemplateField}}" userInput="{{template.templateName}}" stepKey="findCreatedTemplate"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearch"/> + <waitForElementVisible selector="{{AdminEmailTemplateIndexSection.templateRowByName(template.templateName)}}" stepKey="waitForTemplatesAppeared"/> + <click selector="{{AdminEmailTemplateIndexSection.templateRowByName(template.templateName)}}" stepKey="clickToOpenTemplate"/> + <waitForElementVisible selector="{{AdminEmailTemplateEditSection.templateCode}}" stepKey="waitForTemplateNameisible"/> + <seeInField selector="{{AdminEmailTemplateEditSection.templateCode}}" userInput="{{template.templateName}}" stepKey="checkTemplateName"/> + </actionGroup> + + <actionGroup name="DeleteEmailTemplate" extends="FindAndOpenEmailTemplate"> + <click selector="{{AdminEmailTemplateEditSection.deleteTemplateButton}}" after="checkTemplateName" stepKey="deleteTemplate"/> + <acceptPopup after="deleteTemplate" stepKey="acceptPopup"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" after="acceptPopup" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the email template." after="waitForSuccessMessage" stepKey="seeSuccessfulMessage"/> </actionGroup> - <!--Delete created Template--> - <actionGroup name="DeleteCreatedTemplate"> - <switchToPreviousTab stepKey="switchToPreviousTab"/> - <seeInCurrentUrl stepKey="seeCreatedTemplateUrl" url="email_template/edit/id"/> - <click stepKey="clickDeleteTemplateButton" selector="{{EmailTemplatesSection.deleteTemplateButton}}"/> - <acceptPopup stepKey="acceptDeletingTemplatePopUp"/> - <see stepKey="SeeSuccessfulMessage" userInput="You deleted the email template."/> - <click stepKey="clickResetFilterButton" selector="{{EmailTemplatesSection.resetFilterButton}}"/> - <waitForElementNotVisible selector="{{MarketingEmailTemplateSection.clearSearchTemplate(EmailTemplate.templateName)}}" stepKey="waitForSearchFieldCleared"/> + <actionGroup name="PreviewEmailTemplate" extends="FindAndOpenEmailTemplate"> + <click selector="{{AdminEmailTemplateEditSection.previewTemplateButton}}" after="checkTemplateName" stepKey="clickPreviewTemplate"/> + <switchToNextTab after="clickPreviewTemplate" stepKey="switchToNewOpenedTab"/> + <seeInCurrentUrl url="{{AdminEmailTemplatePreviewPage.url}}" after="switchToNewOpenedTab" stepKey="seeCurrentUrl"/> + <seeElement selector="{{AdminEmailTemplatePreviewSection.iframe}}" after="seeCurrentUrl" stepKey="seeIframeOnPage"/> + <switchToIFrame userInput="preview_iframe" after="seeIframeOnPage" stepKey="switchToIframe"/> + <waitForPageLoad after="switchToIframe" stepKey="waitForPageLoaded"/> </actionGroup> + <actionGroup name="AssertEmailTemplateContent"> + <arguments> + <argument name="expectedContent" type="string" defaultValue="{{EmailTemplate.templateText}}"/> + </arguments> + + <see userInput="{{expectedContent}}" stepKey="checkTemplateContainText"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Email/Test/Mftf/Data/EmailTemplateData.xml b/app/code/Magento/Email/Test/Mftf/Data/EmailTemplateData.xml index 04e597244833a..7f28e2241761b 100644 --- a/app/code/Magento/Email/Test/Mftf/Data/EmailTemplateData.xml +++ b/app/code/Magento/Email/Test/Mftf/Data/EmailTemplateData.xml @@ -10,5 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="EmailTemplate" type="template"> <data key="templateName" unique="suffix">Template</data> + <data key="templateSubject" unique="suffix">Template Subject_</data> + <data key="templateText" unique="suffix">Template Text_</data> </entity> </entities> diff --git a/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplateEditPage.xml b/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplateEditPage.xml new file mode 100644 index 0000000000000..f369e84abf374 --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplateEditPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:/Page/etc/PageObject.xsd"> + <page name="AdminEmailTemplateEditPage" url="/admin/email_template/edit/id/{{templateId}}/" area="admin" module="Magento_Email" parameterized="true"> + <section name="AdminEmailTemplateEditSection"/> + </page> +</pages> diff --git a/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplatePage.xml b/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplateIndexPage.xml similarity index 65% rename from app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplatePage.xml rename to app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplateIndexPage.xml index 9636986dda8fa..c4ba7aa006203 100644 --- a/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplatePage.xml +++ b/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplateIndexPage.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="AdminEmailTemplatePage" url="/admin/email_template/" area="admin" module="Email"> - <section name="AdminEmailTemplatePageActionSection"/> + <page name="AdminEmailTemplateIndexPage" url="/admin/email_template/" area="admin" module="Magento_Email"> + <section name="AdminEmailTemplateIndexSection"/> </page> </pages> diff --git a/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplatePreviewPage.xml b/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplatePreviewPage.xml new file mode 100644 index 0000000000000..aae010be27fd8 --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Page/AdminEmailTemplatePreviewPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:/Page/etc/PageObject.xsd"> + <page name="AdminEmailTemplatePreviewPage" url="/admin/email_template/preview/" area="admin" module="Magento_Email"> + <section name="AdminEmailTemplatePreviewSection"/> + </page> +</pages> diff --git a/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateEditSection.xml b/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateEditSection.xml new file mode 100644 index 0000000000000..4dd4ac14cc040 --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateEditSection.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="AdminEmailTemplateEditSection"> + <element name="templateCode" type="input" selector="#template_code"/> + <element name="templateSubject" type="input" selector="#template_subject"/> + <element name="templateText" type="input" selector="#template_text"/> + <element name="templateDropDown" type="select" selector="#template_select"/> + <element name="loadTemplateButton" type="button" selector="#load" timeout="30"/> + <element name="deleteTemplateButton" type="button" selector="#delete"/> + <element name="previewTemplateButton" type="button" selector="#preview" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateIndexSection.xml b/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateIndexSection.xml new file mode 100644 index 0000000000000..54d5c54627047 --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplateIndexSection.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="AdminEmailTemplateIndexSection"> + <element name="searchTemplateField" type="input" selector="#systemEmailTemplateGrid_filter_code"/> + <element name="templateRowByName" type="button" selector="//*[@id='systemEmailTemplateGrid_table']//td[contains(@class, 'col-code') and normalize-space(.)='{{templateName}}']/ancestor::tr" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplatePreviewSection.xml b/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplatePreviewSection.xml new file mode 100644 index 0000000000000..5510800edc06e --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Section/AdminEmailTemplatePreviewSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEmailTemplatePreviewSection"> + <element name="iframe" type="iframe" selector="#preview_iframe"/> + </section> +</sections> diff --git a/app/code/Magento/Email/Test/Mftf/Section/EmailTemplateSection.xml b/app/code/Magento/Email/Test/Mftf/Section/EmailTemplateSection.xml deleted file mode 100644 index 4e877bd24239e..0000000000000 --- a/app/code/Magento/Email/Test/Mftf/Section/EmailTemplateSection.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?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="MarketingEmailTemplateSection"> - <element name="searchTemplateField" type="input" selector="#systemEmailTemplateGrid_filter_code"/> - <element name="searchButton" type="button" selector="//*[@title='Search' and @class='action-default scalable action-secondary']"/> - <element name="createdTemplate" type="button" selector="//*[normalize-space() ='{{arg}}']" parameterized="true"/> - <element name="clearSearchTemplate" type="input" selector="//*[@id='systemEmailTemplateGrid_filter_code' and @value='{{arg2}}']" parameterized="true"/> - </section> - - <section name="EmailTemplatesSection"> - <element name="addNewTemplateButton" type="button" selector="#add"/> - <element name="templateDropDown" type="select" selector="#template_select"/> - <element name="loadTemplateButton" type="button" selector="#load"/> - <element name="templateNameField" type="input" selector="#template_code"/> - <element name="saveTemplateButton" type="button" selector="#save"/> - <element name="previewTemplateButton" type="button" selector="#preview"/> - <element name="deleteTemplateButton" type="button" selector="#delete"/> - <element name="resetFilterButton" type="button" selector="//span[contains(text(),'Reset Filter')]"/> - </section> - -</sections> diff --git a/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewTest.xml b/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewTest.xml new file mode 100644 index 0000000000000..459e3c0f9290f --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEmailTemplatePreviewTest"> + <annotations> + <features value="Email"/> + <stories value="Create email template"/> + <title value="Check email template preview"/> + <description value="Check if email template preview works correctly"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15794"/> + <useCaseId value="MC-11050"/> + <group value="email"/> + </annotations> + + <before> + <!--Login to Admin Area--> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + </before> + + <after> + <!--Delete created Template--> + <actionGroup ref="DeleteEmailTemplate" stepKey="deleteTemplate"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clearFilters"/> + <!--Logout from Admin Area--> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <actionGroup ref="CreateCustomTemplate" stepKey="createTemplate"/> + <actionGroup ref="PreviewEmailTemplate" stepKey="previewTemplate"/> + <actionGroup ref="AssertEmailTemplateContent" stepKey="assertContent"/> + </test> +</tests> diff --git a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml index c3870417fa5e0..9e1d9c5c3cdbb 100644 --- a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml +++ b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml @@ -17,6 +17,9 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13908"/> <group value="LogoUpload"/> + <skip> + <issueId value="MC-18496"/> + </skip> </annotations> <!--Login to Admin Area--> <before> diff --git a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php index e11d7fc4db534..286e9a989d4d8 100644 --- a/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php +++ b/app/code/Magento/Email/Test/Unit/Block/Adminhtml/Template/PreviewTest.php @@ -38,14 +38,16 @@ public function testToHtml($requestParamMap) { $storeId = 1; $template = $this->getMockBuilder(\Magento\Email\Model\Template::class) - ->setMethods([ - 'setDesignConfig', - 'getDesignConfig', - '__wakeup', - 'getProcessedTemplate', - 'getAppState', - 'revertDesign' - ]) + ->setMethods( + [ + 'setDesignConfig', + 'getDesignConfig', + '__wakeup', + 'getProcessedTemplate', + 'getAppState', + 'revertDesign' + ] + ) ->disableOriginalConstructor() ->getMock(); $template->expects($this->once()) @@ -55,9 +57,7 @@ public function testToHtml($requestParamMap) $designConfigData = []; $template->expects($this->atLeastOnce()) ->method('getDesignConfig') - ->willReturn(new \Magento\Framework\DataObject( - $designConfigData - )); + ->willReturn(new \Magento\Framework\DataObject($designConfigData)); $emailFactory = $this->createPartialMock(\Magento\Email\Model\TemplateFactory::class, ['create']); $emailFactory->expects($this->any()) ->method('create') @@ -79,9 +79,7 @@ public function testToHtml($requestParamMap) $storeManager->expects($this->any())->method('getDefaultStoreView')->willReturn(null); $storeManager->expects($this->any())->method('getStores')->willReturn([$store]); $appState = $this->getMockBuilder(\Magento\Framework\App\State::class) - ->setConstructorArgs([ - $scopeConfig - ]) + ->setConstructorArgs([$scopeConfig]) ->setMethods(['emulateAreaCode']) ->disableOriginalConstructor() ->getMock(); @@ -128,16 +126,6 @@ public function toHtmlDataProvider() { return [ ['data 1' => [ - ['type', null, ''], - ['text', null, sprintf('<javascript>%s</javascript>', self::MALICIOUS_TEXT)], - ['styles', null, ''], - ]], - ['data 2' => [ - ['type', null, ''], - ['text', null, sprintf('<iframe>%s</iframe>', self::MALICIOUS_TEXT)], - ['styles', null, ''], - ]], - ['data 3' => [ ['type', null, ''], ['text', null, self::MALICIOUS_TEXT], ['styles', null, ''], diff --git a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php index 0ba717a461718..9a67bf59dd4bf 100644 --- a/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php +++ b/app/code/Magento/Email/Test/Unit/Controller/Adminhtml/Email/Template/PreviewTest.php @@ -15,9 +15,7 @@ use Magento\Framework\View\Result\Page; /** - * Preview Test - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * Preview Test. */ class PreviewTest extends \PHPUnit\Framework\TestCase { @@ -122,7 +120,7 @@ public function testExecute() ->willReturnSelf(); $this->responseMock->expects($this->once()) ->method('setHeader') - ->with('Content-Security-Policy', "script-src 'none'"); + ->with('Content-Security-Policy', "script-src 'self'"); $this->assertNull($this->object->execute()); } diff --git a/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_popup.xml b/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_popup.xml new file mode 100644 index 0000000000000..d633ac8ccf9e1 --- /dev/null +++ b/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_popup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <container name="root"> + <block class="Magento\Framework\View\Element\Template" name="page.block" template="Magento_Email::template/preview.phtml"> + <block class="Magento\Email\Block\Adminhtml\Template\Preview" name="content" as="content"/> + </block> + </container> +</layout> diff --git a/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_preview.xml b/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_preview.xml index b308ad7d3dfcd..97f31c618f9b7 100644 --- a/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_preview.xml +++ b/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_preview.xml @@ -6,12 +6,13 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - <update handle="empty" /> <body> + <attribute name="id" value="html-body"/> + <attribute name="class" value="preview-window"/> + <referenceContainer name="backend.page" remove="true"/> + <referenceContainer name="menu.wrapper" remove="true"/> <referenceContainer name="root"> - <block name="preview.page.content" class="Magento\Framework\View\Element\Template" template="Magento_Email::template/preview.phtml"> - <block class="Magento\Email\Block\Adminhtml\Template\Preview" name="content" as="content"/> - </block> + <block name="preview.page.content" class="Magento\Backend\Block\Page" template="Magento_Email::preview/iframeswitcher.phtml"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml new file mode 100644 index 0000000000000..4d26b59b093e2 --- /dev/null +++ b/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Backend\Block\Page $block */ +?> +<div id="preview" class="cms-revision-preview"> + <iframe + name="preview_iframe" + id="preview_iframe" + frameborder="0" + title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" + width="100%" + sandbox="allow-forms allow-pointer-lock" + src="<?= $block->escapeUrl($block->getUrl('*/*/popup', ['_current' => true])) ?>" + /> +</div> 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 8b1f50095d984..9653156e85e80 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -48,7 +48,7 @@ use Magento\Framework\App\TemplateTypesInterface; <?= /* @noEscape */ $block->getFormHtml() ?> </form> -<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="email_template_preview_form" target="_blank"> +<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="get" id="email_template_preview_form" target="_blank"> <?= /* @noEscape */ $block->getBlockHtml('formkey') ?> <div class="no-display"> <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>" /> diff --git a/app/code/Magento/Email/view/adminhtml/web/js/variables.js b/app/code/Magento/Email/view/adminhtml/web/js/variables.js index d519053b5265a..7a671823ace02 100644 --- a/app/code/Magento/Email/view/adminhtml/web/js/variables.js +++ b/app/code/Magento/Email/view/adminhtml/web/js/variables.js @@ -21,6 +21,7 @@ define([ overlayShowEffectOptions: null, overlayHideEffectOptions: null, insertFunction: 'Variables.insertVariable', + variablesValue: [], /** * @param {*} textareaElementId @@ -52,11 +53,12 @@ define([ this.variablesContent = '<ul class="insert-variable">'; variables.each(function (variableGroup) { if (variableGroup.label && variableGroup.value) { - this.variablesContent += '<li><b>' + variableGroup.label + '</b></li>'; + this.variablesContent += '<li><b>' + variableGroup.label.escapeHTML() + '</b></li>'; variableGroup.value.each(function (variable) { if (variable.value && variable.label) { + this.variablesValue.push(variable.value); this.variablesContent += '<li>' + - this.prepareVariableRow(variable.value, variable.label) + '</li>'; + this.prepareVariableRow(this.variablesValue.length, variable.label) + '</li>'; } }.bind(this)); } @@ -75,7 +77,7 @@ define([ openDialogWindow: function (variablesContent) { var windowId = this.dialogWindowId; - jQuery('<div id="' + windowId + '">' + Variables.variablesContent + '</div>').modal({ + jQuery('<div id="' + windowId + '">' + variablesContent + '</div>').modal({ title: $t('Insert Variable...'), type: 'slide', buttons: [], @@ -87,8 +89,6 @@ define([ }); jQuery('#' + windowId).modal('openModal'); - - variablesContent.evalScripts.bind(variablesContent).defer(); }, /** @@ -99,27 +99,24 @@ define([ }, /** - * @param {String} varValue + * @param {Number} index * @param {*} varLabel * @return {String} */ - prepareVariableRow: function (varValue, varLabel) { - var value = varValue.replace(/"/g, '"').replace(/'/g, '\\''), - content = '<a href="#" onclick="' + - this.insertFunction + - '(\'' + - value + - '\');return false;">' + - varLabel + - '</a>'; - - return content; + prepareVariableRow: function (index, varLabel) { + return '<a href="#" onclick="' + + this.insertFunction + + '(' + + index + + ');return false;">' + + varLabel.escapeHTML() + + '</a>'; }, /** - * @param {*} value + * @param {*} variable */ - insertVariable: function (value) { + insertVariable: function (variable) { var windowId = this.dialogWindowId, textareaElm, scrollPos; @@ -128,14 +125,17 @@ define([ if (textareaElm) { scrollPos = textareaElm.scrollTop; - updateElementAtCursor(textareaElm, value); + + if (!isNaN(variable)) { + updateElementAtCursor(textareaElm, Variables.variablesValue[variable - 1]); + } else { + updateElementAtCursor(textareaElm, variable); + } textareaElm.focus(); textareaElm.scrollTop = scrollPos; jQuery(textareaElm).change(); textareaElm = null; } - - return; } }; @@ -172,8 +172,6 @@ define([ } else { this.openChooser(this.variables); } - - return; }, /** @@ -194,8 +192,6 @@ define([ Variables.closeDialogWindow(); this.editor.execCommand('mceInsertContent', false, value); } - - return; } }; }); diff --git a/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php index 7c026e7d4136e..d2f285c0b988b 100644 --- a/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php +++ b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php @@ -15,6 +15,7 @@ use Magento\PageCache\Model\Config; use Magento\GraphQl\Controller\HttpRequestProcessor; use Magento\Framework\App\Response\Http as ResponseHttp; +use Magento\Framework\Registry; /** * Plugin for handling controller after controller tags and pre-controller validation. @@ -41,22 +42,30 @@ class GraphQl */ private $requestProcessor; + /** + * @var Registry + */ + private $registry; + /** * @param CacheableQuery $cacheableQuery * @param Config $config * @param HttpResponse $response * @param HttpRequestProcessor $requestProcessor + * @param Registry $registry */ public function __construct( CacheableQuery $cacheableQuery, Config $config, HttpResponse $response, - HttpRequestProcessor $requestProcessor + HttpRequestProcessor $requestProcessor, + Registry $registry ) { $this->cacheableQuery = $cacheableQuery; $this->config = $config; $this->response = $response; $this->requestProcessor = $requestProcessor; + $this->registry = $registry; } /** @@ -89,6 +98,9 @@ public function afterRenderResult(ResultInterface $subject, ResultInterface $res { $sendNoCacheHeaders = false; if ($this->config->isEnabled()) { + /** @see \Magento\Framework\App\Http::launch */ + /** @see \Magento\PageCache\Model\Controller\Result\BuiltinPlugin::afterRenderResult */ + $this->registry->register('use_page_cache_plugin', true, true); if ($this->cacheableQuery->shouldPopulateCacheHeadersWithTags()) { $this->response->setPublicHeaders($this->config->getTtl()); $this->response->setHeader('X-Magento-Tags', implode(',', $this->cacheableQuery->getCacheTags()), true); diff --git a/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php index 7e624845f5682..53f5155f8a3ac 100644 --- a/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php +++ b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php @@ -9,6 +9,7 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Request\Http; use Magento\GraphQlCache\Model\Resolver\IdentityPool; /** @@ -53,29 +54,20 @@ public function __construct( * Set cache validity to the cacheableQuery after resolving any resolver or evaluating a promise in a query * * @param array $resolvedValue - * @param Field $field + * @param array $cacheAnnotation Eg: ['cacheable' => true, 'cacheTag' => 'someTag', cacheIdentity=>'\Mage\Class'] * @return void */ - public function handleCacheFromResolverResponse(array $resolvedValue, Field $field) : void + public function handleCacheFromResolverResponse(array $resolvedValue, array $cacheAnnotation) : void { - $cache = $field->getCache(); - $cacheIdentityClass = $cache['cacheIdentity'] ?? ''; - $cacheable = $cache['cacheable'] ?? true; - $cacheTag = $cache['cacheTag'] ?? null; + $cacheable = $cacheAnnotation['cacheable'] ?? true; + $cacheIdentityClass = $cacheAnnotation['cacheIdentity'] ?? ''; - $cacheTags = []; - if ($cacheTag && $this->request->isGet()) { - if (!empty($cacheIdentityClass)) { - $cacheIdentity = $this->identityPool->get($cacheIdentityClass); - $cacheTagIds = $cacheIdentity->getIdentities($resolvedValue); - if (!empty($cacheTagIds)) { - $cacheTags[] = $cacheTag; - foreach ($cacheTagIds as $cacheTagId) { - $cacheTags[] = $cacheTag . '_' . $cacheTagId; - } - } - } + if ($this->request instanceof Http && $this->request->isGet() && !empty($cacheIdentityClass)) { + $cacheIdentity = $this->identityPool->get($cacheIdentityClass); + $cacheTags = $cacheIdentity->getIdentities($resolvedValue); $this->cacheableQuery->addCacheTags($cacheTags); + } else { + $cacheable = false; } $this->setCacheValidity($cacheable); } diff --git a/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php index 54cb5559923af..d505ca6fbb6ea 100644 --- a/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php +++ b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php @@ -55,18 +55,38 @@ public function afterResolve( array $value = null, array $args = null ) { - /** Only if array @see \Magento\Framework\GraphQl\Query\Resolver\Value */ - if (is_array($resolvedValue) && !empty($field->getCache())) { - $this->cacheableQueryHandler->handleCacheFromResolverResponse($resolvedValue, $field); - } elseif ($resolvedValue instanceof \Magento\Framework\GraphQl\Query\Resolver\Value) { - $resolvedValue->then(function () use ($resolvedValue, $field) { - if (is_array($resolvedValue->promise->result) && $field) { - $this->cacheableQueryHandler->handleCacheFromResolverResponse( - $resolvedValue->promise->result, - $field - ); - } - }); + $cacheAnnotation = $field->getCache(); + if (!empty($cacheAnnotation)) { + if (is_array($resolvedValue)) { + $this->cacheableQueryHandler->handleCacheFromResolverResponse( + $resolvedValue, + $cacheAnnotation + ); + } elseif ($resolvedValue instanceof \Magento\Framework\GraphQl\Query\Resolver\Value) { + $resolvedValue->then( + function () use ($resolvedValue, $field, $cacheAnnotation) { + if (is_array($resolvedValue->promise->result)) { + $this->cacheableQueryHandler->handleCacheFromResolverResponse( + $resolvedValue->promise->result, + $cacheAnnotation + ); + } else { + // case if string or integer we pass in a single array element + $this->cacheableQueryHandler->handleCacheFromResolverResponse( + $resolvedValue->promise->result === null ? + [] : [$field->getName() => $resolvedValue->promise->result], + $cacheAnnotation + ); + } + } + ); + } else { + // case if string or integer we pass in a single array element + $this->cacheableQueryHandler->handleCacheFromResolverResponse( + $resolvedValue === null ? [] : [$field->getName() => $resolvedValue], + $cacheAnnotation + ); + } } return $resolvedValue; } diff --git a/app/code/Magento/GraphQlCache/Model/Resolver/IdentityPool.php b/app/code/Magento/GraphQlCache/Model/Resolver/IdentityPool.php index 00ef8762c28ef..e08392148cb4c 100644 --- a/app/code/Magento/GraphQlCache/Model/Resolver/IdentityPool.php +++ b/app/code/Magento/GraphQlCache/Model/Resolver/IdentityPool.php @@ -42,7 +42,7 @@ public function __construct(ObjectManagerInterface $objectManager) public function get(string $identityClass): IdentityInterface { if (!isset($this->identityInstances[$identityClass])) { - $this->identityInstances[$identityClass] = $this->objectManager->create($identityClass); + $this->identityInstances[$identityClass] = $this->objectManager->get($identityClass); } return $this->identityInstances[$identityClass]; } diff --git a/app/code/Magento/GraphQlCache/Test/Unit/Model/CacheableQueryHandlerTest.php b/app/code/Magento/GraphQlCache/Test/Unit/Model/CacheableQueryHandlerTest.php index 9c1be89928215..ea3e7acf898d7 100644 --- a/app/code/Magento/GraphQlCache/Test/Unit/Model/CacheableQueryHandlerTest.php +++ b/app/code/Magento/GraphQlCache/Test/Unit/Model/CacheableQueryHandlerTest.php @@ -60,14 +60,12 @@ public function testhandleCacheFromResolverResponse( 'cacheIdentity' => IdentityInterface::class, 'cacheTag' => 'cat_p' ]; - $fieldMock = $this->createMock(Field::class); $mockIdentity = $this->getMockBuilder($cacheData['cacheIdentity']) ->setMethods(['getIdentities']) ->getMockForAbstractClass(); $this->requestMock->expects($this->once())->method('isGet')->willReturn(true); $this->identityPoolMock->expects($this->once())->method('get')->willReturn($mockIdentity); - $fieldMock->expects($this->once())->method('getCache')->willReturn($cacheData); $mockIdentity->expects($this->once()) ->method('getIdentities') ->with($resolvedData) @@ -76,7 +74,7 @@ public function testhandleCacheFromResolverResponse( $this->cacheableQueryMock->expects($this->once())->method('isCacheable')->willReturn(true); $this->cacheableQueryMock->expects($this->once())->method('setCacheValidity')->with(true); - $this->cacheableQueryHandler->handleCacheFromResolverResponse($resolvedData, $fieldMock); + $this->cacheableQueryHandler->handleCacheFromResolverResponse($resolvedData, $cacheData); } /** @@ -91,7 +89,7 @@ public function resolvedDataProvider(): array "name" => "TesName", "sku" => "TestSku" ], - "identities" => [10], + "identities" => ["cat_p", "cat_p_10"], "expectedCacheTags" => ["cat_p", "cat_p_10"] ] ]; diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php index aade1abc3f5d7..10ae2dc5e58e1 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php @@ -8,7 +8,7 @@ namespace Magento\ImportExport\Controller\Adminhtml\Export\File; use Magento\Backend\App\Action; -use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; @@ -20,10 +20,10 @@ /** * Controller that delete file by name. */ -class Delete extends ExportController implements HttpGetActionInterface +class Delete extends ExportController implements HttpPostActionInterface { /** - * url to this controller + * Url to this controller */ const URL = 'adminhtml/export_file/delete'; diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php index aca7efef72b28..0e6bca26d2062 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php @@ -21,7 +21,7 @@ class Download extends ExportController implements HttpGetActionInterface { /** - * url to this controller + * Url to this controller */ const URL = 'adminhtml/export_file/download/'; diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index 2a4d6904b11b5..04f4111d3a0a8 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -6,13 +6,35 @@ namespace Magento\ImportExport\Model; +use Magento\Eav\Model\Entity\Attribute; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; use Magento\Framework\HTTP\Adapter\FileTransferFactory; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Math\Random; use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\ImportExport\Helper\Data as DataHelper; +use Magento\ImportExport\Model\Export\Adapter\CsvFactory; +use Magento\ImportExport\Model\Import\AbstractEntity as ImportAbstractEntity; +use Magento\ImportExport\Model\Import\AbstractSource; +use Magento\ImportExport\Model\Import\Adapter; +use Magento\ImportExport\Model\Import\ConfigInterface; +use Magento\ImportExport\Model\Import\Entity\AbstractEntity; +use Magento\ImportExport\Model\Import\Entity\Factory; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\Framework\Message\ManagerInterface; +use Magento\ImportExport\Model\ResourceModel\Import\Data; +use Magento\ImportExport\Model\Source\Import\AbstractBehavior; +use Magento\ImportExport\Model\Source\Import\Behavior\Factory as BehaviorFactory; +use Magento\MediaStorage\Model\File\Uploader; +use Magento\MediaStorage\Model\File\UploaderFactory; +use Psr\Log\LoggerInterface; /** * Import model @@ -20,32 +42,20 @@ * @api * * @method string getBehavior() getBehavior() - * @method \Magento\ImportExport\Model\Import setEntity() setEntity(string $value) + * @method self setEntity() setEntity(string $value) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.TooManyFields) * @since 100.0.2 */ -class Import extends \Magento\ImportExport\Model\AbstractModel +class Import extends AbstractModel { - /**#@+ - * Import behaviors - */ const BEHAVIOR_APPEND = 'append'; - const BEHAVIOR_ADD_UPDATE = 'add_update'; - const BEHAVIOR_REPLACE = 'replace'; - const BEHAVIOR_DELETE = 'delete'; - const BEHAVIOR_CUSTOM = 'custom'; - /**#@-*/ - - /**#@+ - * Form field names (and IDs) - */ - /** * Import source file. */ @@ -91,8 +101,6 @@ class Import extends \Magento\ImportExport\Model\AbstractModel */ const FIELDS_ENCLOSURE = 'fields_enclosure'; - /**#@-*/ - /** * default delimiter for several values in one cell as default for FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR */ @@ -102,27 +110,20 @@ class Import extends \Magento\ImportExport\Model\AbstractModel * default empty attribute value constant */ const DEFAULT_EMPTY_ATTRIBUTE_VALUE_CONSTANT = '__EMPTY__VALUE__'; - - /**#@+ - * Import constants - */ const DEFAULT_SIZE = 50; - const MAX_IMPORT_CHUNKS = 4; - const IMPORT_HISTORY_DIR = 'import_history/'; - const IMPORT_DIR = 'import/'; - /**#@-*/ - - /**#@-*/ + /** + * @var AbstractEntity|ImportAbstractEntity + */ protected $_entityAdapter; /** * Import export data * - * @var \Magento\ImportExport\Helper\Data + * @var DataHelper */ protected $_importExportData = null; @@ -133,46 +134,47 @@ class Import extends \Magento\ImportExport\Model\AbstractModel /** * @var \Magento\ImportExport\Model\Import\ConfigInterface + * @var ConfigInterface */ protected $_importConfig; /** - * @var \Magento\ImportExport\Model\Import\Entity\Factory + * @var Factory */ protected $_entityFactory; /** - * @var \Magento\ImportExport\Model\ResourceModel\Import\Data + * @var Data */ protected $_importData; /** - * @var \Magento\ImportExport\Model\Export\Adapter\CsvFactory + * @var CsvFactory */ protected $_csvFactory; /** - * @var \Magento\Framework\HTTP\Adapter\FileTransferFactory + * @var FileTransferFactory */ protected $_httpFactory; /** - * @var \Magento\MediaStorage\Model\File\UploaderFactory + * @var UploaderFactory */ protected $_uploaderFactory; /** - * @var \Magento\Framework\Indexer\IndexerRegistry + * @var IndexerRegistry */ protected $indexerRegistry; /** - * @var \Magento\ImportExport\Model\Source\Import\Behavior\Factory + * @var BehaviorFactory */ protected $_behaviorFactory; /** - * @var \Magento\Framework\Filesystem + * @var Filesystem */ protected $_filesystem; @@ -192,41 +194,48 @@ class Import extends \Magento\ImportExport\Model\AbstractModel private $messageManager; /** - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\ImportExport\Helper\Data $importExportData - * @param \Magento\Framework\App\Config\ScopeConfigInterface $coreConfig + * @var Random + */ + private $random; + + /** + * @param LoggerInterface $logger + * @param Filesystem $filesystem + * @param DataHelper $importExportData + * @param ScopeConfigInterface $coreConfig * @param Import\ConfigInterface $importConfig * @param Import\Entity\Factory $entityFactory - * @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData + * @param Data $importData * @param Export\Adapter\CsvFactory $csvFactory * @param FileTransferFactory $httpFactory - * @param \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory + * @param UploaderFactory $uploaderFactory * @param Source\Import\Behavior\Factory $behaviorFactory - * @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry + * @param IndexerRegistry $indexerRegistry * @param History $importHistoryModel * @param DateTime $localeDate * @param array $data * @param ManagerInterface|null $messageManager + * @param Random|null $random * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Psr\Log\LoggerInterface $logger, - \Magento\Framework\Filesystem $filesystem, - \Magento\ImportExport\Helper\Data $importExportData, - \Magento\Framework\App\Config\ScopeConfigInterface $coreConfig, - \Magento\ImportExport\Model\Import\ConfigInterface $importConfig, - \Magento\ImportExport\Model\Import\Entity\Factory $entityFactory, - \Magento\ImportExport\Model\ResourceModel\Import\Data $importData, - \Magento\ImportExport\Model\Export\Adapter\CsvFactory $csvFactory, - \Magento\Framework\HTTP\Adapter\FileTransferFactory $httpFactory, - \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory, - \Magento\ImportExport\Model\Source\Import\Behavior\Factory $behaviorFactory, - \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, - \Magento\ImportExport\Model\History $importHistoryModel, + LoggerInterface $logger, + Filesystem $filesystem, + DataHelper $importExportData, + ScopeConfigInterface $coreConfig, + ConfigInterface $importConfig, + Factory $entityFactory, + Data $importData, + CsvFactory $csvFactory, + FileTransferFactory $httpFactory, + UploaderFactory $uploaderFactory, + BehaviorFactory $behaviorFactory, + IndexerRegistry $indexerRegistry, + History $importHistoryModel, DateTime $localeDate, array $data = [], - ManagerInterface $messageManager = null + ManagerInterface $messageManager = null, + Random $random = null ) { $this->_importExportData = $importExportData; $this->_coreConfig = $coreConfig; @@ -241,15 +250,18 @@ public function __construct( $this->_filesystem = $filesystem; $this->importHistoryModel = $importHistoryModel; $this->localeDate = $localeDate; - $this->messageManager = $messageManager ?: ObjectManager::getInstance()->get(ManagerInterface::class); + $this->messageManager = $messageManager ?: ObjectManager::getInstance() + ->get(ManagerInterface::class); + $this->random = $random ?: ObjectManager::getInstance() + ->get(Random::class); parent::__construct($logger, $filesystem, $data); } /** * Create instance of entity adapter and return it * - * @throws \Magento\Framework\Exception\LocalizedException - * @return \Magento\ImportExport\Model\Import\Entity\AbstractEntity|\Magento\ImportExport\Model\Import\AbstractEntity + * @throws LocalizedException + * @return AbstractEntity|ImportAbstractEntity */ protected function _getEntityAdapter() { @@ -260,30 +272,30 @@ protected function _getEntityAdapter() $this->_entityAdapter = $this->_entityFactory->create($entities[$this->getEntity()]['model']); } catch (\Exception $e) { $this->_logger->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Please enter a correct entity model.') ); } - if (!$this->_entityAdapter instanceof \Magento\ImportExport\Model\Import\Entity\AbstractEntity && - !$this->_entityAdapter instanceof \Magento\ImportExport\Model\Import\AbstractEntity + if (!$this->_entityAdapter instanceof AbstractEntity && + !$this->_entityAdapter instanceof ImportAbstractEntity ) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __( 'The entity adapter object must be an instance of %1 or %2.', - \Magento\ImportExport\Model\Import\Entity\AbstractEntity::class, - \Magento\ImportExport\Model\Import\AbstractEntity::class + AbstractEntity::class, + ImportAbstractEntity::class ) ); } // check for entity codes integrity if ($this->getEntity() != $this->_entityAdapter->getEntityTypeCode()) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('The input entity code is not equal to entity adapter code.') ); } } else { - throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a correct entity.')); + throw new LocalizedException(__('Please enter a correct entity.')); } $this->_entityAdapter->setParameters($this->getData()); } @@ -294,12 +306,12 @@ protected function _getEntityAdapter() * Returns source adapter object. * * @param string $sourceFile Full path to source file - * @return \Magento\ImportExport\Model\Import\AbstractSource - * @throws \Magento\Framework\Exception\FileSystemException + * @return AbstractSource + * @throws FileSystemException */ protected function _getSourceAdapter($sourceFile) { - return \Magento\ImportExport\Model\Import\Adapter::findAdapterFor( + return Adapter::findAdapterFor( $sourceFile, $this->_filesystem->getDirectoryWrite(DirectoryList::ROOT), $this->getData(self::FIELD_FIELD_SEPARATOR) @@ -311,7 +323,7 @@ protected function _getSourceAdapter($sourceFile) * * @param ProcessingErrorAggregatorInterface $validationResult * @return string[] - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getOperationResultMessages(ProcessingErrorAggregatorInterface $validationResult) { @@ -354,10 +366,11 @@ public function getOperationResultMessages(ProcessingErrorAggregatorInterface $v /** * Get attribute type for upcoming validation. * - * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute|\Magento\Eav\Model\Entity\Attribute $attribute + * @param AbstractAttribute|Attribute $attribute * @return string + * phpcs:disable Magento2.Functions.StaticFunction */ - public static function getAttributeType(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute) + public static function getAttributeType(AbstractAttribute $attribute) { $frontendInput = $attribute->getFrontendInput(); if ($attribute->usesSource() && in_array($frontendInput, ['select', 'multiselect', 'boolean'])) { @@ -372,7 +385,7 @@ public static function getAttributeType(\Magento\Eav\Model\Entity\Attribute\Abst /** * DB data source model getter. * - * @return \Magento\ImportExport\Model\ResourceModel\Import\Data + * @return Data */ public function getDataSourceModel() { @@ -393,14 +406,19 @@ public static function getDefaultBehavior() /** * Override standard entity getter. * - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @return string */ public function getEntity() { - if (empty($this->_data['entity'])) { - throw new \Magento\Framework\Exception\LocalizedException(__('Entity is unknown')); + $entities = $this->_importConfig->getEntities(); + + if (empty($this->_data['entity']) + || !empty($this->_data['entity']) && !isset($entities[$this->_data['entity']]) + ) { + throw new LocalizedException(__('Entity is unknown')); } + return $this->_data['entity']; } @@ -408,7 +426,7 @@ public function getEntity() * Returns number of checked entities. * * @return int - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getProcessedEntitiesCount() { @@ -419,7 +437,7 @@ public function getProcessedEntitiesCount() * Returns number of checked rows. * * @return int - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getProcessedRowsCount() { @@ -440,7 +458,7 @@ public function getWorkingDir() * Import source file structure to DB. * * @return bool - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function importSource() { @@ -477,7 +495,7 @@ public function importSource() * Process import. * * @return bool - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ protected function processImport() { @@ -488,7 +506,7 @@ protected function processImport() * Import possibility getter. * * @return bool - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function isImportAllowed() { @@ -499,7 +517,7 @@ public function isImportAllowed() * Get error aggregator instance. * * @return ProcessingErrorAggregatorInterface - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getErrorAggregator() { @@ -509,7 +527,7 @@ public function getErrorAggregator() /** * Move uploaded file. * - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @return string Source file path */ public function uploadSource() @@ -523,20 +541,22 @@ public function uploadSource() } else { $errorMessage = __('The file was not uploaded.'); } - throw new \Magento\Framework\Exception\LocalizedException($errorMessage); + throw new LocalizedException($errorMessage); } $entity = $this->getEntity(); - /** @var $uploader \Magento\MediaStorage\Model\File\Uploader */ + /** @var $uploader Uploader */ $uploader = $this->_uploaderFactory->create(['fileId' => self::FIELD_NAME_SOURCE_FILE]); $uploader->skipDbProcessing(true); - $result = $uploader->save($this->getWorkingDir()); + $fileName = $this->random->getRandomString(32) . '.' . $uploader->getFileExtension(); + $result = $uploader->save($this->getWorkingDir(), $fileName); + // phpcs:disable Magento2.Functions.DiscouragedFunction.Discouraged $extension = pathinfo($result['file'], PATHINFO_EXTENSION); $uploadedFile = $result['path'] . $result['file']; if (!$extension) { $this->_varDirectory->delete($uploadedFile); - throw new \Magento\Framework\Exception\LocalizedException(__('The file you uploaded has no extension.')); + throw new LocalizedException(__('The file you uploaded has no extension.')); } $sourceFile = $this->getWorkingDir() . $entity; @@ -553,8 +573,8 @@ public function uploadSource() $this->_varDirectory->getRelativePath($uploadedFile), $sourceFileRelative ); - } catch (\Magento\Framework\Exception\FileSystemException $e) { - throw new \Magento\Framework\Exception\LocalizedException(__('The source file moving process failed.')); + } catch (FileSystemException $e) { + throw new LocalizedException(__('The source file moving process failed.')); } } $this->_removeBom($sourceFile); @@ -566,8 +586,7 @@ public function uploadSource() * Move uploaded file and provide source instance. * * @return Import\AbstractSource - * @throws \Magento\Framework\Exception\FileSystemException - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function uploadFileAndGetSource() { @@ -576,7 +595,7 @@ public function uploadFileAndGetSource() $source = $this->_getSourceAdapter($sourceFile); } catch (\Exception $e) { $this->_varDirectory->delete($this->_varDirectory->getRelativePath($sourceFile)); - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage())); + throw new LocalizedException(__($e->getMessage())); } return $source; @@ -587,7 +606,7 @@ public function uploadFileAndGetSource() * * @param string $sourceFile * @return $this - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException */ protected function _removeBom($sourceFile) { @@ -605,11 +624,11 @@ protected function _removeBom($sourceFile) * Before validate data the method requires to initialize error aggregator (ProcessingErrorAggregatorInterface) * with 'validation strategy' and 'allowed error count' values to allow using this parameters in validation process. * - * @param \Magento\ImportExport\Model\Import\AbstractSource $source + * @param AbstractSource $source * @return bool - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ - public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $source) + public function validateSource(AbstractSource $source) { $this->addLogComment(__('Begin data validation')); @@ -624,7 +643,7 @@ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $adapter->validateData(); } catch (\Exception $e) { $errorAggregator->addError( - \Magento\ImportExport\Model\Import\Entity\AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION, + AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION, ProcessingError::ERROR_LEVEL_CRITICAL, null, null, @@ -646,7 +665,7 @@ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource * Invalidate indexes by process codes. * * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function invalidateIndex() { @@ -662,6 +681,7 @@ public function invalidateIndex() if (!$indexer->isScheduled()) { $indexer->invalidate(); } + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (\InvalidArgumentException $e) { } } @@ -680,7 +700,7 @@ public function invalidateIndex() * ) * * @return array - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getEntityBehaviors() { @@ -689,7 +709,7 @@ public function getEntityBehaviors() foreach ($entities as $entityCode => $entityData) { $behaviorClassName = isset($entityData['behaviorModel']) ? $entityData['behaviorModel'] : null; if ($behaviorClassName && class_exists($behaviorClassName)) { - /** @var $behavior \Magento\ImportExport\Model\Source\Import\AbstractBehavior */ + /** @var $behavior AbstractBehavior */ $behavior = $this->_behaviorFactory->create($behaviorClassName); $behaviourData[$entityCode] = [ 'token' => $behaviorClassName, @@ -697,7 +717,7 @@ public function getEntityBehaviors() 'notes' => $behavior->getNotes($entityCode), ]; } else { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('The behavior token for %1 is invalid.', $entityCode) ); } @@ -713,7 +733,7 @@ public function getEntityBehaviors() * ) * * @return array - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getUniqueEntityBehaviors() { @@ -733,7 +753,7 @@ public function getUniqueEntityBehaviors() * * @param string|null $entity * @return bool - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function isReportEntityType($entity = null) { @@ -747,12 +767,12 @@ public function isReportEntityType($entity = null) try { $result = $this->_getEntityAdapter()->isNeedToLogInHistory(); } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Please enter a correct entity model') ); } } else { - throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a correct entity model')); + throw new LocalizedException(__('Please enter a correct entity model')); } } else { $result = $this->_getEntityAdapter()->isNeedToLogInHistory(); @@ -768,7 +788,7 @@ public function isReportEntityType($entity = null) * @param string $extension * @param array $result * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ protected function createHistoryReport($sourceFileRelative, $entity, $extension = null, $result = null) { @@ -781,6 +801,7 @@ protected function createHistoryReport($sourceFileRelative, $entity, $extension } elseif ($extension !== null) { $fileName = $entity . $extension; } else { + // phpcs:disable Magento2.Functions.DiscouragedFunction.Discouraged $fileName = basename($sourceFileRelative); } $copyName = $this->localeDate->gmtTimestamp() . '_' . $fileName; @@ -792,8 +813,8 @@ protected function createHistoryReport($sourceFileRelative, $entity, $extension $content = $this->_varDirectory->getDriver()->fileGetContents($sourceFileRelative); $this->_varDirectory->writeFile($copyFile, $content); } - } catch (\Magento\Framework\Exception\FileSystemException $e) { - throw new \Magento\Framework\Exception\LocalizedException(__('Source file coping failed')); + } catch (FileSystemException $e) { + throw new LocalizedException(__('Source file coping failed')); } $this->importHistoryModel->addReport($copyName); } @@ -804,7 +825,7 @@ protected function createHistoryReport($sourceFileRelative, $entity, $extension * Get count of created items * * @return int - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getCreatedItemsCount() { @@ -815,7 +836,7 @@ public function getCreatedItemsCount() * Get count of updated items * * @return int - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getUpdatedItemsCount() { @@ -826,7 +847,7 @@ public function getUpdatedItemsCount() * Get count of deleted items * * @return int - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getDeletedItemsCount() { diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php index 823ec29d41760..50e71512c3d28 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php @@ -141,19 +141,23 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->errorAggregatorMock = $this->getErrorAggregatorObject([ - 'initValidationStrategy', - 'getErrorsCount', - ]); + $this->errorAggregatorMock = $this->getErrorAggregatorObject( + [ + 'initValidationStrategy', + 'getErrorsCount', + ] + ); $this->_entityAdapter = $this->getMockBuilder(\Magento\ImportExport\Model\Import\Entity\AbstractEntity::class) ->disableOriginalConstructor() - ->setMethods([ - 'importData', - '_saveValidatedBunches', - 'getErrorAggregator', - 'setSource', - 'validateData', - ]) + ->setMethods( + [ + 'importData', + '_saveValidatedBunches', + 'getErrorAggregator', + 'setSource', + 'validateData', + ] + ) ->getMockForAbstractClass(); $this->_entityAdapter->method('getErrorAggregator') ->willReturn($this->errorAggregatorMock); @@ -201,33 +205,37 @@ protected function setUp() ->method('getDriver') ->willReturn($this->_driver); $this->import = $this->getMockBuilder(\Magento\ImportExport\Model\Import::class) - ->setConstructorArgs([ - $logger, - $this->_filesystem, - $this->_importExportData, - $this->_coreConfig, - $this->_importConfig, - $this->_entityFactory, - $this->_importData, - $this->_csvFactory, - $this->_httpFactory, - $this->_uploaderFactory, - $this->_behaviorFactory, - $this->indexerRegistry, - $this->historyModel, - $this->dateTime - ]) - ->setMethods([ - 'getDataSourceModel', - 'setData', - 'getData', - 'getProcessedEntitiesCount', - 'getProcessedRowsCount', - 'getEntity', - 'getBehavior', - 'isReportEntityType', - '_getEntityAdapter' - ]) + ->setConstructorArgs( + [ + $logger, + $this->_filesystem, + $this->_importExportData, + $this->_coreConfig, + $this->_importConfig, + $this->_entityFactory, + $this->_importData, + $this->_csvFactory, + $this->_httpFactory, + $this->_uploaderFactory, + $this->_behaviorFactory, + $this->indexerRegistry, + $this->historyModel, + $this->dateTime + ] + ) + ->setMethods( + [ + 'getDataSourceModel', + 'setData', + 'getData', + 'getProcessedEntitiesCount', + 'getProcessedRowsCount', + 'getEntity', + 'getBehavior', + 'isReportEntityType', + '_getEntityAdapter' + ] + ) ->getMock(); $this->setPropertyValue($this->import, '_varDirectory', $this->_varDirectory); } @@ -459,10 +467,12 @@ public function testValidateSource() $this->import->expects($this->any()) ->method('getData') - ->willReturnMap([ - [Import::FIELD_NAME_VALIDATION_STRATEGY, null, $validationStrategy], - [Import::FIELD_NAME_ALLOWED_ERROR_COUNT, null, $allowedErrorCount], - ]); + ->willReturnMap( + [ + [Import::FIELD_NAME_VALIDATION_STRATEGY, null, $validationStrategy], + [Import::FIELD_NAME_ALLOWED_ERROR_COUNT, null, $allowedErrorCount], + ] + ); $this->assertTrue($this->import->validateSource($csvMock)); @@ -499,12 +509,16 @@ public function testInvalidateIndex() $this->_importConfig->expects($this->atLeastOnce()) ->method('getRelatedIndexers') ->willReturn($indexers); + $this->_importConfig->method('getEntities') + ->willReturn(['test' => []]); $this->indexerRegistry->expects($this->any()) ->method('get') - ->willReturnMap([ - ['indexer_1', $indexer1], - ['indexer_2', $indexer2], - ]); + ->willReturnMap( + [ + ['indexer_1', $indexer1], + ['indexer_2', $indexer2], + ] + ); $import = new Import( $logger, @@ -532,6 +546,8 @@ public function testInvalidateIndexWithoutIndexers() $this->_importConfig->expects($this->once()) ->method('getRelatedIndexers') ->willReturn([]); + $this->_importConfig->method('getEntities') + ->willReturn(['test' => []]); $logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) ->disableOriginalConstructor() @@ -558,12 +574,78 @@ public function testInvalidateIndexWithoutIndexers() $this->assertSame($import, $import->invalidateIndex()); } + public function testGetKnownEntity() + { + $this->_importConfig->method('getEntities') + ->willReturn(['test' => []]); + + $logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $import = new Import( + $logger, + $this->_filesystem, + $this->_importExportData, + $this->_coreConfig, + $this->_importConfig, + $this->_entityFactory, + $this->_importData, + $this->_csvFactory, + $this->_httpFactory, + $this->_uploaderFactory, + $this->_behaviorFactory, + $this->indexerRegistry, + $this->historyModel, + $this->dateTime + ); + + $import->setEntity('test'); + $entity = $import->getEntity(); + self::assertSame('test', $entity); + } + /** - * @todo to implement it. + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Entity is unknown + * @dataProvider unknownEntitiesProvider */ - public function testGetEntityBehaviors() + public function testGetUnknownEntity($entity) { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->_importConfig->method('getEntities') + ->willReturn(['test' => []]); + + $logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $import = new Import( + $logger, + $this->_filesystem, + $this->_importExportData, + $this->_coreConfig, + $this->_importConfig, + $this->_entityFactory, + $this->_importData, + $this->_csvFactory, + $this->_httpFactory, + $this->_uploaderFactory, + $this->_behaviorFactory, + $this->indexerRegistry, + $this->historyModel, + $this->dateTime + ); + + $import->setEntity($entity); + $import->getEntity(); + } + + public function unknownEntitiesProvider() + { + return [ + [''], + ['foo'], + ]; } /** @@ -583,9 +665,9 @@ public function testIsReportEntityType($entity, $getEntityResult, $expectedResul { $importMock = $this->getMockBuilder(\Magento\ImportExport\Model\Import::class) ->disableOriginalConstructor() - ->setMethods([ - 'getEntity', '_getEntityAdapter', 'getEntityTypeCode', 'isNeedToLogInHistory' - ]) + ->setMethods( + ['getEntity', '_getEntityAdapter', 'getEntityTypeCode', 'isNeedToLogInHistory'] + ) ->getMock(); $importMock->expects($this->any())->method('_getEntityAdapter')->willReturnSelf(); $importMock->expects($this->any())->method('getEntityTypeCode')->willReturn('catalog_product'); @@ -621,9 +703,9 @@ public function testIsReportEntityTypeException($entity, $getEntitiesResult, $ge { $importMock = $this->getMockBuilder(\Magento\ImportExport\Model\Import::class) ->disableOriginalConstructor() - ->setMethods([ - 'getEntity', '_getEntityAdapter', 'getEntityTypeCode', 'isNeedToLogInHistory' - ]) + ->setMethods( + ['getEntity', '_getEntityAdapter', 'getEntityTypeCode', 'isNeedToLogInHistory'] + ) ->getMock(); $importMock->expects($this->any())->method('_getEntityAdapter')->willReturnSelf(); $importMock->expects($this->any())->method('getEntityTypeCode')->willReturn('catalog_product'); @@ -834,9 +916,11 @@ public function testCreateHistoryReportThrowException() $this->_driver ->expects($this->any()) ->method('fileGetContents') - ->willReturnCallback(function () use ($phrase) { - throw new \Magento\Framework\Exception\FileSystemException($phrase); - }); + ->willReturnCallback( + function () use ($phrase) { + throw new \Magento\Framework\Exception\FileSystemException($phrase); + } + ); $this->dateTime ->expects($this->once()) ->method('gmtTimestamp') diff --git a/app/code/Magento/ImportExport/Ui/Component/Columns/ExportGridActions.php b/app/code/Magento/ImportExport/Ui/Component/Columns/ExportGridActions.php index a7b9b072f00f4..b5e36ccd9fbab 100644 --- a/app/code/Magento/ImportExport/Ui/Component/Columns/ExportGridActions.php +++ b/app/code/Magento/ImportExport/Ui/Component/Columns/ExportGridActions.php @@ -65,7 +65,8 @@ public function prepareDataSource(array $dataSource) 'confirm' => [ 'title' => __('Delete'), 'message' => __('Are you sure you wan\'t to delete a file?') - ] + ], + 'post' => true, ]; } } diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml new file mode 100644 index 0000000000000..085a710f2671c --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectMultishipping"> + <!--todo MC-5858: some urls don't redirect to https--> + <annotations> + <features value="Multishipping"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Multishipping Pages"/> + <description value="Verify that the Secure URL configuration applies to the Multishipping pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15611"/> + <group value="multishipping"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="_defaultProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="moveMouseOverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="clickAddToCartButton"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForAddedToCartSuccessMessage"/> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$product.name$$ to your shopping cart." stepKey="seeAddedToCartSuccessMessage"/> + <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/multishipping/checkout" stepKey="goToUnsecureMultishippingCheckoutURL"/> + <seeInCurrentUrl url="https://{$hostname}/multishipping/checkout" stepKey="seeSecureMultishippingCheckoutURL"/> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminNewsletterTemplateActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminNewsletterTemplateActionGroup.xml new file mode 100644 index 0000000000000..bd6842f785ecf --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminNewsletterTemplateActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SwitchToPreviewIframeActionGroup"> + <executeJS function="document.getElementById('preview_iframe').sandbox.add('allow-scripts')" stepKey="addSandboxValue"/> + <wait time="10" stepKey="waitBeforeSwitchToIframe"/> + <switchToIFrame userInput="preview_iframe" stepKey="switchToIframe" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml index a343a20a6d57c..510f3e16e8d8e 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml @@ -54,8 +54,7 @@ <waitForPageLoad stepKey="waitForPageLoad11"/> <click selector="{{NewsletterWYSIWYGSection.Preview(_defaultNewsletter.name)}}" stepKey="clickPreview"/> <switchToWindow stepKey="switchToWindow" userInput="action_window"/> - <switchToIFrame userInput="preview_iframe" stepKey="switchToIframe" /> - <waitForPageLoad stepKey="waitForPageLoad9"/> + <actionGroup ref="SwitchToPreviewIframeActionGroup" stepKey="switchToIframe"/> <!--Verify that the text and image are present--> <seeElement selector="{{StorefrontNewsletterSection.mediaDescription}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontNewsletterSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddVariableToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddVariableToWYSIWYGNewsletterTest.xml index 841d202d518ab..b6f78b6f33792 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddVariableToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddVariableToWYSIWYGNewsletterTest.xml @@ -83,8 +83,7 @@ <waitForPageLoad stepKey="waitForPageLoad9" /> <click selector="{{NewsletterWYSIWYGSection.Preview(_defaultNewsletter.name)}}" stepKey="clickPreview1" /> <switchToWindow userInput="action_window" stepKey="switchToWindow1"/> - <switchToIFrame userInput="preview_iframe" stepKey="switchToIframe1" /> - <waitForPageLoad stepKey="waitForPageLoad7"/> + <actionGroup ref="SwitchToPreviewIframeActionGroup" stepKey="switchToIframe"/> <!--see Default Variable on Storefront--> <see userInput="{{_defaultVariable.city}}" stepKey="seeDefaultVariable" /> <!--see Custom Variable on Storefront--> @@ -95,8 +94,7 @@ <amOnPage url="{{NewsletterTemplateGrid.url}}" stepKey="amOnTemplateGrid" /> <click selector="{{NewsletterWYSIWYGSection.Preview(_defaultNewsletter.name)}}" stepKey="clickPreview2" /> <switchToWindow userInput="action_window" stepKey="switchToWindow2"/> - <switchToIFrame userInput="preview_iframe" stepKey="switchToIframe2" /> - <wait time="10" stepKey="waitForPageLoad8"/> + <actionGroup ref="SwitchToPreviewIframeActionGroup" stepKey="switchToIframeAfterVariableDelete"/> <!--see custom variable blank--> <dontSee userInput="{{customVariable.html}}" stepKey="dontSeeCustomVariableName" /> <closeTab stepKey="closeTab"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml index 016f07b8a2f44..a7ac9e38d4b07 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml @@ -51,7 +51,7 @@ <waitForPageLoad stepKey="waitForPageLoad10" /> <click selector="{{NewsletterWYSIWYGSection.Preview(_defaultNewsletter.name)}}" stepKey="clickPreview" /> <switchToWindow stepKey="switchToWindow" userInput="action_window"/> - <switchToIFrame userInput="preview_iframe" stepKey="switchToIframe" /> + <actionGroup ref="SwitchToPreviewIframeActionGroup" stepKey="switchToIframe"/> <waitForText userInput="Home page" stepKey="waitForPageLoad9"/> <see userInput="Home page" stepKey="seeHomePageCMSPage"/> <closeTab stepKey="closeTab"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml new file mode 100644 index 0000000000000..01b5e706fcefb --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectNewsletter"> + <annotations> + <features value="Newsletter"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Newsletter Pages"/> + <description value="Verify that the Secure URL configuration applies to the Newsletter pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15584"/> + <group value="newsletter"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/newsletter/manage" stepKey="goToUnsecureNewsletterManageURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/newsletter/manage" stepKey="seeSecureNewsletterManageURL"/> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnNewsletterTest.xml index c4636fea3fb91..722a0dd22119d 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnNewsletterTest.xml @@ -43,7 +43,7 @@ <waitForPageLoad stepKey="waitForPageLoad3" /> <click selector="{{NewsletterWYSIWYGSection.Preview(_defaultNewsletter.name)}}" stepKey="clickPreview" /> <switchToWindow stepKey="switchToWindow" userInput="action_window"/> - <switchToIFrame userInput="preview_iframe" stepKey="switchToIframe" /> + <actionGroup ref="SwitchToPreviewIframeActionGroup" stepKey="switchToIframe"/> <waitForText userInput="Hello World From Newsletter Template!" stepKey="waitForPageLoad2"/> <see userInput="Hello World From Newsletter Template!" stepKey="seeContent" /> <closeTab stepKey="closeTab"/> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml index 00f430cd14b8d..5175080add914 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -20,7 +20,7 @@ frameborder="0" title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" width="100%" - sandbox="allow-forms allow-pointer-lock allow-scripts" + sandbox="allow-forms allow-pointer-lock" > </iframe> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml new file mode 100644 index 0000000000000..b2fcfa43181dc --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectPaypal"> + <annotations> + <features value="Paypal"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Paypal Pages"/> + <description value="Verify that the Secure URL configuration applies to the Paypal pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15541"/> + <group value="paypal"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/paypal/billing" stepKey="goToUnsecurePaypalBillingURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/billing" stepKey="seeSecurePaypalBillingURL"/> + <amOnUrl url="http://{$hostname}/paypal/billing_agreement" stepKey="goToUnsecurePaypalBillingAgreementURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/billing_agreement" stepKey="seeSecurePaypalBillingAgreementURL"/> + <amOnUrl url="http://{$hostname}/paypal/bml" stepKey="goToUnsecurePaypalBmlURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/bml" stepKey="seeSecurePaypalBmlURL"/> + <amOnUrl url="http://{$hostname}/paypal/hostedpro" stepKey="goToUnsecurePaypalHostedProURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/hostedpro" stepKey="seeSecurePaypalHostedProURL"/> + <amOnUrl url="http://{$hostname}/paypal/ipn" stepKey="goToUnsecurePaypalIpnURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/ipn" stepKey="seeSecurePaypalIpnURL"/> + <amOnUrl url="http://{$hostname}/paypal/payflow" stepKey="goToUnsecurePaypalPayflowUL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/payflow" stepKey="seeSecurePaypalPayflowURL"/> + <amOnUrl url="http://{$hostname}/paypal/payflowadvanced" stepKey="goToUnsecurePaypalPayflowAdvancedURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/payflowadvanced" stepKey="seeSecurePaypalPayflowAdvancedURL"/> + <amOnUrl url="http://{$hostname}/paypal/payflowbml" stepKey="goToUnsecurePaypalPayflowBmlURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/payflowbml" stepKey="seeSecurePaypalPayflowBmlURL"/> + <amOnUrl url="http://{$hostname}/paypal/payflowexpress" stepKey="goToUnsecurePaypalPayflowExpressURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/payflowexpress" stepKey="seeSecurePaypalPayflowExpressURL"/> + <amOnUrl url="http://{$hostname}/paypal/transparent" stepKey="goToUnsecurePaypalTransparentURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/transparent" stepKey="seeSecurePaypalTransparentURL"/> + <amOnUrl url="http://{$hostname}/paypal/express" stepKey="goToUnsecurePaypalExpressURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/paypal/express" stepKey="seeSecurePaypalExpressURL"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php new file mode 100644 index 0000000000000..e4de60cafb8ad --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Fieldset; + +/** + * Class GroupTest + */ +class GroupTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Group + */ + protected $_model; + + /** + * @var \Magento\Framework\Data\Form\Element\AbstractElement + */ + protected $_element; + + /** + * @var \Magento\Backend\Model\Auth\Session|\PHPUnit_Framework_MockObject_MockObject + */ + protected $_authSession; + + /** + * @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject + */ + protected $_user; + + /** + * @var \Magento\Config\Model\Config\Structure\Element\Group|\PHPUnit_Framework_MockObject_MockObject + */ + protected $_group; + + protected function setUp() + { + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->_group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class); + $this->_element = $this->getMockForAbstractClass( + \Magento\Framework\Data\Form\Element\AbstractElement::class, + [], + '', + false, + true, + true, + ['getHtmlId', 'getElementHtml', 'getName', 'getElements', 'getId'] + ); + $this->_element->expects($this->any()) + ->method('getHtmlId') + ->will($this->returnValue('html id')); + $this->_element->expects($this->any()) + ->method('getElementHtml') + ->will($this->returnValue('element html')); + $this->_element->expects($this->any()) + ->method('getName') + ->will($this->returnValue('name')); + $this->_element->expects($this->any()) + ->method('getElements') + ->will($this->returnValue([])); + $this->_element->expects($this->any()) + ->method('getId') + ->will($this->returnValue('id')); + $this->_user = $this->createMock(\Magento\User\Model\User::class); + $this->_authSession = $this->createMock(\Magento\Backend\Model\Auth\Session::class); + $this->_authSession->expects($this->any()) + ->method('__call') + ->with('getUser') + ->will($this->returnValue($this->_user)); + $this->_model = $helper->getObject( + \Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Group::class, + ['authSession' => $this->_authSession] + ); + $this->_model->setGroup($this->_group); + } + + /** + * @param mixed $expanded + * @param int $expected + * @dataProvider isCollapseStateDataProvider + */ + public function testIsCollapseState($expanded, $expected) + { + $this->_user->setExtra(['configState' => []]); + $this->_element->setGroup(isset($expanded) ? ['expanded' => $expanded] : []); + $html = $this->_model->render($this->_element); + $this->assertContains( + '<input id="' . $this->_element->getHtmlId() . '-state" name="config_state[' + . $this->_element->getId() . ']" type="hidden" value="' . $expected . '" />', + $html + ); + } + + /** + * @return array + */ + public function isCollapseStateDataProvider() + { + return [ + [null, 0], + [false, 0], + ['', 0], + [1, 1], + ['1', 1], + ]; + } +} diff --git a/app/code/Magento/Paypal/etc/frontend/di.xml b/app/code/Magento/Paypal/etc/frontend/di.xml index 858eb21d4e74a..a728f5583a8d6 100644 --- a/app/code/Magento/Paypal/etc/frontend/di.xml +++ b/app/code/Magento/Paypal/etc/frontend/di.xml @@ -40,6 +40,7 @@ <arguments> <argument name="secureUrlList" xsi:type="array"> <item name="paypal_billing" xsi:type="string">/paypal/billing/</item> + <item name="paypal_billing_agreement" xsi:type="string">/paypal/billing_agreement/</item> <item name="paypal_bml" xsi:type="string">/paypal/bml/</item> <item name="paypal_hostedpro" xsi:type="string">/paypal/hostedpro/</item> <item name="paypal_ipn" xsi:type="string">/paypal/ipn/</item> diff --git a/app/code/Magento/Quote/Model/CouponManagement.php b/app/code/Magento/Quote/Model/CouponManagement.php index 55c21c974d6dd..0c93724ae12bc 100644 --- a/app/code/Magento/Quote/Model/CouponManagement.php +++ b/app/code/Magento/Quote/Model/CouponManagement.php @@ -7,6 +7,7 @@ namespace Magento\Quote\Model; +use Magento\Framework\Exception\LocalizedException; use \Magento\Quote\Api\CouponManagementInterface; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; @@ -36,7 +37,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function get($cartId) { @@ -46,7 +47,7 @@ public function get($cartId) } /** - * {@inheritdoc} + * @inheritDoc */ public function set($cartId, $couponCode) { @@ -63,9 +64,12 @@ public function set($cartId, $couponCode) try { $quote->setCouponCode($couponCode); $this->quoteRepository->save($quote->collectTotals()); + } catch (LocalizedException $e) { + throw new CouldNotSaveException(__('The coupon code couldn\'t be applied: ' .$e->getMessage()), $e); } catch (\Exception $e) { throw new CouldNotSaveException( - __("The coupon code couldn't be applied. Verify the coupon code and try again.") + __("The coupon code couldn't be applied. Verify the coupon code and try again."), + $e ); } if ($quote->getCouponCode() != $couponCode) { @@ -75,7 +79,7 @@ public function set($cartId, $couponCode) } /** - * {@inheritdoc} + * @inheritDoc */ public function remove($cartId) { diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 893a0b9df458d..897227bbcf30b 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -141,9 +141,6 @@ input PaymentMethodInput { additional_data: PaymentMethodAdditionalDataInput @doc(description: "Additional payment data") } -input PaymentMethodAdditionalDataInput { -} - input SetGuestEmailOnCartInput { cart_id: String! email: String! @@ -198,7 +195,7 @@ type Cart { email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail") shipping_addresses: [ShippingCartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") billing_address: BillingCartAddress! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress") - available_payment_methods: [AvailablePaymentMethod] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AvailablePaymentMethods") @doc(description: "Available payment methods") + available_payment_methods: [AvailablePaymentMethod] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AvailablePaymentMethods") @doc(description: "Available payment methods") selected_payment_method: SelectedPaymentMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SelectedPaymentMethod") prices: CartPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartPrices") } diff --git a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php index b86f8dff2b3b1..813c5f28bf4d9 100644 --- a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php +++ b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php @@ -12,8 +12,10 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Backend\Model\Auth\Session; use Magento\Framework\App\CacheInterface; -use Magento\User\Model\User; +/** + * Class CanViewNotificationTest + */ class CanViewNotificationTest extends \PHPUnit\Framework\TestCase { /** @var CanViewNotification */ @@ -34,11 +36,6 @@ class CanViewNotificationTest extends \PHPUnit\Framework\TestCase /** @var $cacheStorageMock \PHPUnit_Framework_MockObject_MockObject|CacheInterface */ private $cacheStorageMock; - /** - * @var User|\PHPUnit_Framework_MockObject_MockObject - */ - private $userMock; - public function setUp() { $this->cacheStorageMock = $this->getMockBuilder(CacheInterface::class) @@ -47,6 +44,7 @@ public function setUp() ->getMock(); $this->sessionMock = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() + ->setMethods(['getUser', 'getId']) ->getMock(); $this->viewerLoggerMock = $this->getMockBuilder(Logger::class) ->disableOriginalConstructor() @@ -54,7 +52,6 @@ public function setUp() $this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->userMock = $this->createMock(User::class); $objectManager = new ObjectManager($this); $this->canViewNotification = $objectManager->getObject( CanViewNotification::class, @@ -71,8 +68,8 @@ public function testIsVisibleLoadDataFromCache() { $this->sessionMock->expects($this->once()) ->method('getUser') - ->willReturn($this->userMock); - $this->userMock->expects($this->once()) + ->willReturn($this->sessionMock); + $this->sessionMock->expects($this->once()) ->method('getId') ->willReturn(1); $this->cacheStorageMock->expects($this->once()) @@ -96,8 +93,8 @@ public function testIsVisible($expected, $version, $lastViewVersion) ->willReturn(false); $this->sessionMock->expects($this->once()) ->method('getUser') - ->willReturn($this->userMock); - $this->userMock->expects($this->once()) + ->willReturn($this->sessionMock); + $this->sessionMock->expects($this->once()) ->method('getId') ->willReturn(1); $this->productMetadataMock->expects($this->once()) diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php index c5600fe061003..260685395e106 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add.php @@ -93,13 +93,14 @@ protected function _construct() if( response.error ) { alert(response.message); } else if( response.id ){ + var productName = response.name; $("product_id").value = response.id; $("product_name").innerHTML = \'<a href="' . $this->getUrl( 'catalog/product/edit' ) . - 'id/\' + response.id + \'" target="_blank">\' + response.name + \'</a>\'; + 'id/\' + response.id + \'" target="_blank">\' + productName.escapeHTML() + \'</a>\'; } else if ( response.message ) { alert(response.message); } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php index ee74a2c569dc6..1b9c9eaa22be7 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php @@ -11,7 +11,7 @@ use Magento\Review\Model\Review; /** - * Delete action. + * Delete review action. */ class Delete extends ProductController implements HttpPostActionInterface { diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml new file mode 100644 index 0000000000000..b10af7a303cc7 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectReview"> + <annotations> + <features value="Review"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Review Pages"/> + <description value="Verify that the Secure URL configuration applies to the Review pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15542"/> + <group value="review"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/review/customer" stepKey="goToUnsecureReviewURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/review/customer" stepKey="seeSecureReviewURL"/> + </test> +</tests> diff --git a/app/code/Magento/Rule/Block/Editable.php b/app/code/Magento/Rule/Block/Editable.php index 0ea85dcff36c5..57bbc72ff2660 100644 --- a/app/code/Magento/Rule/Block/Editable.php +++ b/app/code/Magento/Rule/Block/Editable.php @@ -58,9 +58,9 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele '" name="' . $this->escapeHtmlAttr($element->getName()) . '" value="' . - $element->getValue() . + $this->escapeHtmlAttr($element->getValue()) . '" data-form-part="' . - $element->getData('data-form-part') . + $this->escapeHtmlAttr($element->getData('data-form-part')) . '"/> ' . $this->escapeHtml( $valueName diff --git a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php index 626dcf2a5a474..689d02c8eefe2 100644 --- a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php +++ b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Block\Order\Info\Buttons; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Model\Rss\Signature; + /** * Block of links in Order view page * @@ -28,20 +31,29 @@ class Rss extends \Magento\Framework\View\Element\Template */ protected $rssUrlBuilder; + /** + * @var Signature + */ + private $signature; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Sales\Model\OrderFactory $orderFactory * @param \Magento\Framework\App\Rss\UrlBuilderInterface $rssUrlBuilder * @param array $data + * @param Signature|null $signature */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Sales\Model\OrderFactory $orderFactory, \Magento\Framework\App\Rss\UrlBuilderInterface $rssUrlBuilder, - array $data = [] + array $data = [], + Signature $signature = null ) { $this->orderFactory = $orderFactory; $this->rssUrlBuilder = $rssUrlBuilder; + $this->signature = $signature ?: ObjectManager::getInstance()->get(Signature::class); + parent::__construct($context, $data); } @@ -103,10 +115,12 @@ protected function getUrlKey($order) protected function getLinkParams() { $order = $this->orderFactory->create()->load($this->_request->getParam('order_id')); + $data = $this->getUrlKey($order); + return [ 'type' => 'order_status', '_secure' => true, - '_query' => ['data' => $this->getUrlKey($order)] + '_query' => ['data' => $data, 'signature' => $this->signature->signData($data)], ]; } } diff --git a/app/code/Magento/Sales/Controller/Guest/View.php b/app/code/Magento/Sales/Controller/Guest/View.php index 9e96a15ed2c12..04e510f116c3f 100644 --- a/app/code/Magento/Sales/Controller/Guest/View.php +++ b/app/code/Magento/Sales/Controller/Guest/View.php @@ -6,11 +6,16 @@ namespace Magento\Sales\Controller\Guest; use Magento\Framework\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Sales\Helper\Guest as GuestHelper; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -class View extends Action\Action +/** + * Guest order view action. + */ +class View extends Action\Action implements HttpPostActionInterface, HttpGetActionInterface { /** * @var \Magento\Sales\Helper\Guest @@ -38,7 +43,7 @@ public function __construct( } /** - * @return \Magento\Framework\Controller\ResultInterface + * @inheritdoc */ public function execute() { diff --git a/app/code/Magento/Sales/Helper/Admin.php b/app/code/Magento/Sales/Helper/Admin.php index ab212f77ce935..0e0d8213cb791 100644 --- a/app/code/Magento/Sales/Helper/Admin.php +++ b/app/code/Magento/Sales/Helper/Admin.php @@ -7,6 +7,8 @@ namespace Magento\Sales\Helper; +use Magento\Framework\App\ObjectManager; + /** * Sales admin helper. */ @@ -32,24 +34,33 @@ class Admin extends \Magento\Framework\App\Helper\AbstractHelper */ protected $escaper; + /** + * @var \DOMDocumentFactory + */ + private $domDocumentFactory; + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Sales\Model\Config $salesConfig * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency * @param \Magento\Framework\Escaper $escaper + * @param \DOMDocumentFactory|null $domDocumentFactory */ public function __construct( \Magento\Framework\App\Helper\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Sales\Model\Config $salesConfig, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, - \Magento\Framework\Escaper $escaper + \Magento\Framework\Escaper $escaper, + \DOMDocumentFactory $domDocumentFactory = null ) { $this->priceCurrency = $priceCurrency; $this->_storeManager = $storeManager; $this->_salesConfig = $salesConfig; $this->escaper = $escaper; + $this->domDocumentFactory = $domDocumentFactory + ?: ObjectManager::getInstance()->get(\DOMDocumentFactory::class); parent::__construct($context); } @@ -150,30 +161,42 @@ public function applySalableProductTypesFilter($collection) public function escapeHtmlWithLinks($data, $allowedTags = null) { if (!empty($data) && is_array($allowedTags) && in_array('a', $allowedTags)) { - $links = []; - $i = 1; - $data = str_replace('%', '%%', $data); - $regexp = "#(?J)<a" - ."(?:(?:\s+(?:(?:href\s*=\s*(['\"])(?<link>.*?)\\1\s*)|(?:\S+\s*=\s*(['\"])(.*?)\\3)\s*)*)|>)" - .">?(?:(?:(?<text>.*?)(?:<\/a\s*>?|(?=<\w))|(?<text>.*)))#si"; - while (preg_match($regexp, $data, $matches)) { - $text = ''; - if (!empty($matches['text'])) { - $text = str_replace('%%', '%', $matches['text']); + $wrapperElementId = uniqid(); + $domDocument = $this->domDocumentFactory->create(); + + $internalErrors = libxml_use_internal_errors(true); + + $data = mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8'); + $domDocument->loadHTML( + '<html><body id="' . $wrapperElementId . '">' . $data . '</body></html>' + ); + + libxml_use_internal_errors($internalErrors); + + $linkTags = $domDocument->getElementsByTagName('a'); + + foreach ($linkTags as $linkNode) { + $linkAttributes = []; + foreach ($linkNode->attributes as $attribute) { + $linkAttributes[$attribute->name] = $attribute->value; + } + + foreach ($linkAttributes as $attributeName => $attributeValue) { + if ($attributeName === 'href') { + $url = $this->filterUrl($attributeValue ?? ''); + $url = $this->escaper->escapeUrl($url); + $linkNode->setAttribute('href', $url); + } else { + $linkNode->removeAttribute($attributeName); + } } - $url = $this->filterUrl($matches['link'] ?? ''); - //Recreate a minimalistic secure a tag - $links[] = sprintf( - '<a href="%s">%s</a>', - $this->escaper->escapeHtml($url), - $this->escaper->escapeHtml($text) - ); - $data = str_replace($matches[0], '%' . $i . '$s', $data); - ++$i; } - $data = $this->escaper->escapeHtml($data, $allowedTags); - return vsprintf($data, $links); + + $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); + preg_match('/<body id="' . $wrapperElementId . '">(.+)<\/body><\/html>$/si', $result, $matches); + $data = !empty($matches) ? $matches[1] : ''; } + return $this->escaper->escapeHtml($data, $allowedTags); } @@ -187,7 +210,6 @@ private function filterUrl(string $url): string { if ($url) { //Revert the sprintf escaping - $url = str_replace('%%', '%', $url); // phpcs:ignore Magento2.Functions.DiscouragedFunction $urlScheme = parse_url($url, PHP_URL_SCHEME); $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; diff --git a/app/code/Magento/Sales/Model/Rss/OrderStatus.php b/app/code/Magento/Sales/Model/Rss/OrderStatus.php index 0da218a316117..c3c4456c6b7ca 100644 --- a/app/code/Magento/Sales/Model/Rss/OrderStatus.php +++ b/app/code/Magento/Sales/Model/Rss/OrderStatus.php @@ -6,10 +6,12 @@ namespace Magento\Sales\Model\Rss; use Magento\Framework\App\Rss\DataProviderInterface; +use Magento\Framework\App\ObjectManager; /** - * Class OrderStatus - * @package Magento\Sales\Model\Rss + * Rss renderer for order statuses. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class OrderStatus implements DataProviderInterface { @@ -55,6 +57,11 @@ class OrderStatus implements DataProviderInterface */ protected $orderFactory; + /** + * @var Signature + */ + private $signature; + /** * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param \Magento\Framework\UrlInterface $urlBuilder @@ -63,6 +70,7 @@ class OrderStatus implements DataProviderInterface * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Sales\Model\OrderFactory $orderFactory * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param Signature|null $signature */ public function __construct( \Magento\Framework\ObjectManagerInterface $objectManager, @@ -71,7 +79,8 @@ public function __construct( \Magento\Sales\Model\ResourceModel\Order\Rss\OrderStatusFactory $orderResourceFactory, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Sales\Model\OrderFactory $orderFactory, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + Signature $signature = null ) { $this->objectManager = $objectManager; $this->urlBuilder = $urlBuilder; @@ -80,6 +89,7 @@ public function __construct( $this->localeDate = $localeDate; $this->orderFactory = $orderFactory; $this->config = $scopeConfig; + $this->signature = $signature ?: ObjectManager::getInstance()->get(Signature::class); } /** @@ -96,7 +106,10 @@ public function isAllowed() } /** + * Get rss data. + * * @return array + * @throws \InvalidArgumentException */ public function getRssData() { @@ -108,6 +121,8 @@ public function getRssData() } /** + * Get cache key. + * * @return string */ public function getCacheKey() @@ -115,12 +130,16 @@ public function getCacheKey() $order = $this->getOrder(); $key = ''; if ($order !== null) { + // phpcs:ignore $key = md5($order->getId() . $order->getIncrementId() . $order->getCustomerId()); } + return 'rss_order_status_data_' . $key; } /** + * Get cache lifetime. + * * @return int */ public function getCacheLifetime() @@ -129,6 +148,8 @@ public function getCacheLifetime() } /** + * Get order. + * * @return \Magento\Sales\Model\Order */ protected function getOrder() @@ -137,8 +158,12 @@ protected function getOrder() return $this->order; } - $data = null; - $json = base64_decode((string)$this->request->getParam('data')); + $data = (string)$this->request->getParam('data'); + if (!$this->signature->isValid($data, (string)$this->request->getParam('signature'))) { + return null; + } + // phpcs:ignore + $json = base64_decode($data); if ($json) { $data = json_decode($json, true); } @@ -154,7 +179,7 @@ protected function getOrder() $order = $this->orderFactory->create(); $order->load($data['order_id']); - if ($order->getIncrementId() !== $data['increment_id'] || $order->getCustomerId() !== $data['customer_id']) { + if (!$this->isOrderSuitable($order, $data)) { $order = null; } $this->order = $order; @@ -162,6 +187,18 @@ protected function getOrder() return $this->order; } + /** + * Check if selected order data correspond incoming data. + * + * @param \Magento\Sales\Model\Order $order + * @param array $data + * @return bool + */ + private function isOrderSuitable(\Magento\Sales\Model\Order $order, array $data): bool + { + return $order->getIncrementId() === $data['increment_id'] && $order->getCustomerId() === $data['customer_id']; + } + /** * Get RSS feed items * @@ -192,9 +229,11 @@ protected function getEntries() $entries[] = ['title' => $title, 'link' => $url, 'description' => $description]; } } - $title = __('Order #%1 created at %2', $this->order->getIncrementId(), $this->localeDate->formatDate( - $this->order->getCreatedAt() - )); + $title = __( + 'Order #%1 created at %2', + $this->order->getIncrementId(), + $this->localeDate->formatDate($this->order->getCreatedAt()) + ); $url = $this->urlBuilder->getUrl('sales/order/view', ['order_id' => $this->order->getId()]); $description = '<p>' . __('Current Status: %1<br/>', $this->order->getStatusLabel()) . __('Total: %1<br/>', $this->order->formatPrice($this->order->getGrandTotal())) . '</p>'; @@ -218,6 +257,8 @@ protected function getHeader() } /** + * Get feeds. + * * @return array */ public function getFeeds() @@ -226,7 +267,7 @@ public function getFeeds() } /** - * {@inheritdoc} + * @inheritdoc */ public function isAuthRequired() { diff --git a/app/code/Magento/Sales/Model/Rss/Signature.php b/app/code/Magento/Sales/Model/Rss/Signature.php new file mode 100644 index 0000000000000..28f8dc15984b4 --- /dev/null +++ b/app/code/Magento/Sales/Model/Rss/Signature.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Sales\Model\Rss; + +use Magento\Framework\Encryption\EncryptorInterface; + +/** + * Class for generating signature. + */ +class Signature +{ + /** + * @var EncryptorInterface + */ + private $encryptor; + + /** + * @param EncryptorInterface $encryptor + */ + public function __construct( + EncryptorInterface $encryptor + ) { + $this->encryptor = $encryptor; + } + + /** + * Sign data. + * + * @param string $data + * @return string + */ + public function signData(string $data): string + { + return $this->encryptor->hash($data); + } + + /** + * Check if valid signature is provided for given data. + * + * @param string $data + * @param string $signature + * @return bool + */ + public function isValid(string $data, string $signature): bool + { + return $this->encryptor->validateHash($data, $signature); + } +} diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml index 85ef563e10db7..315a097eb2323 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml @@ -26,7 +26,7 @@ <actionGroup ref="logout" stepKey="logout"/> </after> - <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="openNewOrder"/> + <actionGroup ref="navigateToNewOrderPageNewCustomer" stepKey="openNewOrder"/> <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="Retailer" stepKey="selectCustomerGroup"/> <waitForPageLoad stepKey="waitForPageLoad"/> <grabValueFrom selector="{{AdminOrderFormAccountSection.group}}" stepKey="grabGroupValue"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml new file mode 100644 index 0000000000000..505493e4e5682 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectSales"> + <annotations> + <features value="Sales"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Sales Pages"/> + <description value="Verify that the Secure URL configuration applies to the Sales pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15607"/> + <group value="sales"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/sales" stepKey="goToUnsecureSalesURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/sales" stepKey="seeSecureSalesURL"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Info/Buttons/RssTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Info/Buttons/RssTest.php index d36952e6aeee1..80780d91a68d6 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Info/Buttons/RssTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Info/Buttons/RssTest.php @@ -6,6 +6,7 @@ namespace Magento\Sales\Test\Unit\Block\Order\Info\Buttons; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Model\Rss\Signature; /** * Class RssTest @@ -43,6 +44,14 @@ class RssTest extends \PHPUnit\Framework\TestCase */ protected $scopeConfigInterface; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Signature + */ + private $signature; + + /** + * @inheritdoc + */ protected function setUp() { $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); @@ -50,6 +59,7 @@ protected function setUp() $this->urlBuilderInterface = $this->createMock(\Magento\Framework\App\Rss\UrlBuilderInterface::class); $this->scopeConfigInterface = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $request = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->signature = $this->createMock(Signature::class); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->rss = $this->objectManagerHelper->getObject( @@ -58,7 +68,8 @@ protected function setUp() 'request' => $request, 'orderFactory' => $this->orderFactory, 'rssUrlBuilder' => $this->urlBuilderInterface, - 'scopeConfig' => $this->scopeConfigInterface + 'scopeConfig' => $this->scopeConfigInterface, + 'signature' => $this->signature, ] ); } @@ -75,15 +86,20 @@ public function testGetLink() $order->expects($this->once())->method('getIncrementId')->will($this->returnValue('100000001')); $this->orderFactory->expects($this->once())->method('create')->will($this->returnValue($order)); - $data = base64_encode(json_encode(['order_id' => 1, 'increment_id' => '100000001', 'customer_id' => 1])); - $link = 'http://magento.com/rss/feed/index/type/order_status?data=' . $data; + $signature = '651932dfc862406b72628d95623bae5ea18242be757b3493b337942d61f834be'; + $this->signature->expects($this->once())->method('signData')->willReturn($signature); + $link = 'http://magento.com/rss/feed/index/type/order_status?data=' . $data .'&signature='.$signature; $this->urlBuilderInterface->expects($this->once())->method('getUrl') - ->with([ - 'type' => 'order_status', - '_secure' => true, - '_query' => ['data' => $data], - ])->will($this->returnValue($link)); + ->with( + [ + 'type' => 'order_status', + '_secure' => true, + '_query' => ['data' => $data, 'signature' => $signature], + ] + ) + ->willReturn($link); + $this->assertEquals($link, $this->rss->getLink()); } diff --git a/app/code/Magento/Sales/Test/Unit/Helper/AdminTest.php b/app/code/Magento/Sales/Test/Unit/Helper/AdminTest.php index 389064b7274a7..286ebd0932b40 100644 --- a/app/code/Magento/Sales/Test/Unit/Helper/AdminTest.php +++ b/app/code/Magento/Sales/Test/Unit/Helper/AdminTest.php @@ -71,7 +71,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->adminHelper = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( + $this->adminHelper = (new ObjectManager($this))->getObject( \Magento\Sales\Helper\Admin::class, [ 'context' => $this->contextMock, @@ -330,72 +330,16 @@ public function applySalableProductTypesFilterDataProvider() } /** - * @param string $data - * @param string $expected - * @param null|array $allowedTags - * @dataProvider escapeHtmlWithLinksDataProvider + * @return void */ - public function testEscapeHtmlWithLinks($data, $expected, $allowedTags = null) + public function testEscapeHtmlWithLinks(): void { + $expected = '<a>some text in tags</a>'; $this->escaperMock ->expects($this->any()) ->method('escapeHtml') ->will($this->returnValue($expected)); - $actual = $this->adminHelper->escapeHtmlWithLinks($data, $allowedTags); + $actual = $this->adminHelper->escapeHtmlWithLinks('<a>some text in tags</a>'); $this->assertEquals($expected, $actual); } - - /** - * @return array - */ - public function escapeHtmlWithLinksDataProvider() - { - return [ - [ - '<a>some text in tags</a>', - '<a>some text in tags</a>', - 'allowedTags' => null - ], - [ - 'Transaction ID: "<a target="_blank" href="https://www.paypal.com/?id=XX123XX">XX123XX</a>"', - 'Transaction ID: "<a target="_blank" href="https://www.paypal.com/?id=XX123XX">XX123XX</a>"', - 'allowedTags' => ['b', 'br', 'strong', 'i', 'u', 'a'] - ], - [ - '<a>some text in tags</a>', - '<a>some text in tags</a>', - 'allowedTags' => ['a'] - ], - 'Not replacement with placeholders' => [ - "<a><script>alert(1)</script></a>", - '<a><script>alert(1)</script></a>', - 'allowedTags' => ['a'] - ], - 'Normal usage, url escaped' => [ - '<a href=\"#\">Foo</a>', - '<a href="#">Foo</a>', - 'allowedTags' => ['a'] - ], - 'Normal usage, url not escaped' => [ - "<a href=http://example.com?foo=1&bar=2&baz[name]=BAZ>Foo</a>", - '<a href="http://example.com?foo=1&bar=2&baz[name]=BAZ">Foo</a>', - 'allowedTags' => ['a'] - ], - 'XSS test' => [ - "<a href=\"javascript:alert(59)\">Foo</a>", - '<a href="#">Foo</a>', - 'allowedTags' => ['a'] - ], - 'Additional regex test' => [ - "<a href=\"http://example1.com\" href=\"http://example2.com\">Foo</a>", - '<a href="http://example1.com">Foo</a>', - 'allowedTags' => ['a'] - ], - 'Break of valid urls' => [ - "<a href=\"http://example.com?foo=text with space\">Foo</a>", - '<a href="#">Foo</a>', - 'allowedTags' => ['a'] - ], - ]; - } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Rss/OrderStatusTest.php b/app/code/Magento/Sales/Test/Unit/Model/Rss/OrderStatusTest.php index ce2d09c71b52e..03080bc0479be 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Rss/OrderStatusTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Rss/OrderStatusTest.php @@ -6,9 +6,11 @@ namespace Magento\Sales\Test\Unit\Model\Rss; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Model\Rss\Signature; /** * Class OrderStatusTest + * * @package Magento\Sales\Model\Rss * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -64,6 +66,11 @@ class OrderStatusTest extends \PHPUnit\Framework\TestCase */ protected $order; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Signature + */ + private $signature; + /** * @var array */ @@ -86,6 +93,9 @@ class OrderStatusTest extends \PHPUnit\Framework\TestCase ], ]; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); @@ -101,17 +111,21 @@ protected function setUp() $this->scopeConfigInterface = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $this->order = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->setMethods([ - '__sleep', - '__wakeup', - 'getIncrementId', - 'getId', - 'getCustomerId', - 'load', - 'getStatusLabel', - 'formatPrice', - 'getGrandTotal', - ])->disableOriginalConstructor()->getMock(); + ->setMethods( + [ + '__sleep', + '__wakeup', + 'getIncrementId', + 'getId', + 'getCustomerId', + 'load', + 'getStatusLabel', + 'formatPrice', + 'getGrandTotal', + ] + ) + ->disableOriginalConstructor() + ->getMock(); $this->order->expects($this->any())->method('getId')->will($this->returnValue(1)); $this->order->expects($this->any())->method('getIncrementId')->will($this->returnValue('100000001')); $this->order->expects($this->any())->method('getCustomerId')->will($this->returnValue(1)); @@ -119,7 +133,7 @@ protected function setUp() $this->order->expects($this->any())->method('formatPrice')->will($this->returnValue('15.00')); $this->order->expects($this->any())->method('getGrandTotal')->will($this->returnValue(15)); $this->order->expects($this->any())->method('load')->with(1)->will($this->returnSelf()); - + $this->signature = $this->createMock(Signature::class); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->model = $this->objectManagerHelper->getObject( \Magento\Sales\Model\Rss\OrderStatus::class, @@ -130,17 +144,33 @@ protected function setUp() 'orderResourceFactory' => $this->orderStatusFactory, 'localeDate' => $this->timezoneInterface, 'orderFactory' => $this->orderFactory, - 'scopeConfig' => $this->scopeConfigInterface + 'scopeConfig' => $this->scopeConfigInterface, + 'signature' => $this->signature, ] ); } + /** + * Positive scenario. + */ public function testGetRssData() { $this->orderFactory->expects($this->once())->method('create')->willReturn($this->order); $requestData = base64_encode('{"order_id":1,"increment_id":"100000001","customer_id":1}'); + $this->signature->expects($this->never())->method('signData'); + $this->signature->expects($this->any()) + ->method('isValid') + ->with($requestData, 'signature') + ->willReturn(true); - $this->requestInterface->expects($this->any())->method('getParam')->with('data')->willReturn($requestData); + $this->requestInterface->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['data', null, $requestData], + ['signature', null, 'signature'], + ] + ); $resource = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Rss\OrderStatus::class) ->setMethods(['getAllCommentCollection']) @@ -162,24 +192,64 @@ public function testGetRssData() } /** + * Case when invalid data is provided. + * * @expectedException \InvalidArgumentException * @expectedExceptionMessage Order not found. */ public function testGetRssDataWithError() { $this->orderFactory->expects($this->once())->method('create')->willReturn($this->order); - $requestData = base64_encode('{"order_id":"1","increment_id":true,"customer_id":true}'); - - $this->requestInterface->expects($this->any())->method('getParam')->with('data')->willReturn($requestData); - + $this->signature->expects($this->never())->method('signData'); + $this->signature->expects($this->any()) + ->method('isValid') + ->with($requestData, 'signature') + ->willReturn(true); + $this->requestInterface->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['data', null, $requestData], + ['signature', null, 'signature'], + ] + ); $this->orderStatusFactory->expects($this->never())->method('create'); - $this->urlInterface->expects($this->never())->method('getUrl'); + $this->assertEquals($this->feedData, $this->model->getRssData()); + } + /** + * Case when invalid signature is provided. + * + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Order not found. + */ + public function testGetRssDataWithWrongSignature() + { + $requestData = base64_encode('{"order_id":"1","increment_id":true,"customer_id":true}'); + $this->signature->expects($this->never()) + ->method('signData'); + $this->signature->expects($this->any()) + ->method('isValid') + ->with($requestData, 'signature') + ->willReturn(false); + $this->requestInterface->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['data', null, $requestData], + ['signature', null, 'signature'], + ] + ); + $this->orderStatusFactory->expects($this->never())->method('create'); + $this->urlInterface->expects($this->never())->method('getUrl'); $this->assertEquals($this->feedData, $this->model->getRssData()); } + /** + * Testing allowed getter. + */ public function testIsAllowed() { $this->scopeConfigInterface->expects($this->once())->method('getValue') @@ -189,6 +259,8 @@ public function testIsAllowed() } /** + * Test caching. + * * @param string $requestData * @param string $result * @dataProvider getCacheKeyDataProvider @@ -196,23 +268,39 @@ public function testIsAllowed() public function testGetCacheKey($requestData, $result) { $this->requestInterface->expects($this->any())->method('getParam') - ->with('data') - ->will($this->returnValue($requestData)); + ->willReturnMap( + [ + ['data', null, $requestData], + ['signature', null, 'signature'], + ] + ); + $this->signature->expects($this->never())->method('signData'); + $this->signature->expects($this->any()) + ->method('isValid') + ->with($requestData, 'signature') + ->willReturn(true); $this->orderFactory->expects($this->once())->method('create')->will($this->returnValue($this->order)); $this->assertEquals('rss_order_status_data_' . $result, $this->model->getCacheKey()); } /** + * Test data for caching test. + * * @return array */ public function getCacheKeyDataProvider() { + // phpcs:disable return [ [base64_encode('{"order_id":1,"increment_id":"100000001","customer_id":1}'), md5('11000000011')], [base64_encode('{"order_id":"1","increment_id":true,"customer_id":true}'), ''] ]; + // phpcs:enable } + /** + * Test for cache lifetime getter. + */ public function testGetCacheLifetime() { $this->assertEquals(600, $this->model->getCacheLifetime()); diff --git a/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/Status/OptionsTest.php b/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/Status/OptionsTest.php index c0eba0b14138a..fe285d29d703b 100644 --- a/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/Status/OptionsTest.php +++ b/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/Status/OptionsTest.php @@ -39,17 +39,34 @@ protected function setUp() public function testToOptionArray() { - $collectionMock = - $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Status\Collection::class); - $options = ['options']; + $collectionMock = $this->createMock( + \Magento\Sales\Model\ResourceModel\Order\Status\Collection::class + ); + + $options = [ + [ + 'value' => '1', + 'label' => 'Label' + ] + ]; + + $expectedOptions = [ + [ + 'value' => '1', + 'label' => 'Label', + '__disableTmpl' => true + ] + ]; $this->collectionFactoryMock->expects($this->once()) ->method('create') ->willReturn($collectionMock); + $collectionMock->expects($this->once()) ->method('toOptionArray') ->willReturn($options); - $this->assertEquals($options, $this->model->toOptionArray()); - $this->assertEquals($options, $this->model->toOptionArray()); + + $this->assertEquals($expectedOptions, $this->model->toOptionArray()); + $this->assertEquals($expectedOptions, $this->model->toOptionArray()); } } diff --git a/app/code/Magento/Sales/Ui/Component/Listing/Column/Status/Options.php b/app/code/Magento/Sales/Ui/Component/Listing/Column/Status/Options.php index e091d4966282a..14964f16b701e 100644 --- a/app/code/Magento/Sales/Ui/Component/Listing/Column/Status/Options.php +++ b/app/code/Magento/Sales/Ui/Component/Listing/Column/Status/Options.php @@ -41,7 +41,16 @@ public function __construct(CollectionFactory $collectionFactory) public function toOptionArray() { if ($this->options === null) { - $this->options = $this->collectionFactory->create()->toOptionArray(); + $options = $this->collectionFactory->create()->toOptionArray(); + + array_walk( + $options, + function (&$option) { + $option['__disableTmpl'] = true; + } + ); + + $this->options = $options; } return $this->options; } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml index 508ac7c252de5..ab5cd49449ece 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml @@ -23,6 +23,7 @@ $orderStoreDate = $block->formatDate( ); $customerUrl = $block->getCustomerViewUrl(); +$allowedAddressHtmlTags = ['b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'sub', 'sup', 'ul']; ?> <section class="admin__page-section order-view-account-information"> @@ -168,7 +169,7 @@ $customerUrl = $block->getCustomerViewUrl(); <span class="title"><?= $block->escapeHtml(__('Billing Address')) ?></span> <div class="actions"><?= /* @noEscape */ $block->getAddressEditLink($order->getBillingAddress()); ?></div> </div> - <address class="admin__page-section-item-content"><?= /* @noEscape */ $block->getFormattedAddress($order->getBillingAddress()); ?></address> + <address class="admin__page-section-item-content"><?= $block->escapeHtml($block->getFormattedAddress($order->getBillingAddress()), $allowedAddressHtmlTags); ?></address> </div> <?php if (!$block->getOrder()->getIsVirtual()) : ?> <div class="admin__page-section-item order-shipping-address"> @@ -177,7 +178,7 @@ $customerUrl = $block->getCustomerViewUrl(); <span class="title"><?= $block->escapeHtml(__('Shipping Address')) ?></span> <div class="actions"><?= /* @noEscape */ $block->getAddressEditLink($order->getShippingAddress()); ?></div> </div> - <address class="admin__page-section-item-content"><?= /* @noEscape */ $block->getFormattedAddress($order->getShippingAddress()); ?></address> + <address class="admin__page-section-item-content"><?= $block->escapeHtml($block->getFormattedAddress($order->getShippingAddress()), $allowedAddressHtmlTags); ?></address> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/SalesRule/Api/CouponManagementInterface.php b/app/code/Magento/SalesRule/Api/CouponManagementInterface.php index dc1c1de81965e..defa800bc1798 100644 --- a/app/code/Magento/SalesRule/Api/CouponManagementInterface.php +++ b/app/code/Magento/SalesRule/Api/CouponManagementInterface.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\SalesRule\Api; /** diff --git a/app/code/Magento/SalesRule/Api/Exception/CodeRequestLimitException.php b/app/code/Magento/SalesRule/Api/Exception/CodeRequestLimitException.php new file mode 100644 index 0000000000000..dfa1c9c1f6a53 --- /dev/null +++ b/app/code/Magento/SalesRule/Api/Exception/CodeRequestLimitException.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Api\Exception; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Thrown when coupon codes requests limit is reached. + */ +class CodeRequestLimitException extends LocalizedException +{ + +} diff --git a/app/code/Magento/SalesRule/Model/Coupon/AdminCodeLimitManager.php b/app/code/Magento/SalesRule/Model/Coupon/AdminCodeLimitManager.php new file mode 100644 index 0000000000000..92217f4b8b567 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/AdminCodeLimitManager.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon; + +use Magento\SalesRule\Model\Spi\CodeLimitManagerInterface; + +/** + * Limit manager for admin area. + */ +class AdminCodeLimitManager implements CodeLimitManagerInterface +{ + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function checkRequest(string $code): void + { + //phpcs:ignore Squiz.PHP.NonExecutableCode.ReturnNotRequired + return; + } +} diff --git a/app/code/Magento/SalesRule/Model/Coupon/CaptchaConfigProvider.php b/app/code/Magento/SalesRule/Model/Coupon/CaptchaConfigProvider.php new file mode 100644 index 0000000000000..0f4e008e672ce --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/CaptchaConfigProvider.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon; + +use Magento\Captcha\Model\DefaultModel; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Captcha\Helper\Data as Helper; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Checkout\Model\ConfigProviderInterface; + +/** + * Adds captcha data related to coupons. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class CaptchaConfigProvider implements ConfigProviderInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Helper + */ + private $captchaData; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @param StoreManagerInterface $storeManager + * @param Helper $captchaData + * @param CustomerSession $session + */ + public function __construct(StoreManagerInterface $storeManager, Helper $captchaData, CustomerSession $session) + { + $this->storeManager = $storeManager; + $this->captchaData = $captchaData; + $this->customerSession = $session; + } + + /** + * @inheritDoc + */ + public function getConfig() + { + $formId = 'sales_rule_coupon_request'; + /** @var Store $store */ + $store = $this->storeManager->getStore(); + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaData->getCaptcha($formId); + $login = ''; + if ($this->customerSession->isLoggedIn()) { + $login = $this->customerSession->getCustomerData()->getEmail(); + } + $required = $captchaModel->isRequired($login); + if ($required) { + $captchaModel->generate(); + $imageSrc = $captchaModel->getImgSrc(); + } else { + $imageSrc = ''; + } + + return [ + 'captcha' => [ + $formId => [ + 'isCaseSensitive' => (bool)$captchaModel->isCaseSensitive(), + 'imageHeight' => $captchaModel->getHeight(), + 'imageSrc' => $imageSrc, + 'refreshUrl' => $store->getUrl('captcha/refresh', ['_secure' => $store->isCurrentlySecure()]), + 'isRequired' => $required, + 'timestamp' => time() + ] + ] + ]; + } +} diff --git a/app/code/Magento/SalesRule/Model/Coupon/CodeLimitManager.php b/app/code/Magento/SalesRule/Model/Coupon/CodeLimitManager.php new file mode 100644 index 0000000000000..0a4ca20268e86 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/CodeLimitManager.php @@ -0,0 +1,180 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\RequestInterface; +use Magento\SalesRule\Api\CouponRepositoryInterface; +use Magento\SalesRule\Api\Exception\CodeRequestLimitException; +use Magento\SalesRule\Model\Spi\CodeLimitManagerInterface; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Observer\CaptchaStringResolver as CaptchaResolver; +use Magento\Captcha\Model\DefaultModel as Captcha; +use Magento\Authorization\Model\UserContextInterface; + +/** + * @inheritDoc + * + * Implementation based on captcha. + */ +class CodeLimitManager implements CodeLimitManagerInterface +{ + /** + * @var CouponRepositoryInterface + */ + private $repository; + + /** + * @var SearchCriteriaBuilder + */ + private $criteriaBuilder; + + /** + * @var CaptchaHelper + */ + private $captchaHelper; + + /** + * @var CaptchaResolver + */ + private $captchaResolver; + + /** + * @var RequestInterface + */ + private $request; + + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * Needed to avoid confusion in case of duplicate checks. + * + * Keys are codes, values are whether captcha was required the 1st time we checked the code. + * + * @var string[] + */ + private $loggedFor = []; + + /** + * Needed to avoid confusion in case of duplicate checks. + * + * @var bool[][] + */ + private $checked = []; + + /** + * @param CouponRepositoryInterface $repository + * @param SearchCriteriaBuilder $criteriaBuilder + * @param CaptchaHelper $captchaHelper + * @param CaptchaResolver $captchaResolver + * @param RequestInterface $request + * @param UserContextInterface $userContext + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + CouponRepositoryInterface $repository, + SearchCriteriaBuilder $criteriaBuilder, + CaptchaHelper $captchaHelper, + CaptchaResolver $captchaResolver, + RequestInterface $request, + UserContextInterface $userContext, + CustomerRepositoryInterface $customerRepository + ) { + $this->repository = $repository; + $this->criteriaBuilder = $criteriaBuilder; + $this->captchaHelper = $captchaHelper; + $this->captchaResolver = $captchaResolver; + $this->request = $request; + $this->userContext = $userContext; + $this->customerRepository = $customerRepository; + } + + /** + * Check whether a valid code was requested. + * + * @param string $code + * @return bool + */ + private function checkCode(string $code): bool + { + $list = $this->repository->getList($this->criteriaBuilder->addFilter('code', $code)->create()); + + return (bool)$list->getTotalCount(); + } + + /** + * Get user's identifier. + * + * @return null|string + */ + private function getLogin(): ?string + { + $login = null; + if ($this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER) { + $login = $this->customerRepository->getById($this->userContext->getUserId())->getEmail(); + } + + return $login; + } + + /** + * @inheritDoc + */ + public function checkRequest(string $code): void + { + $formId = 'sales_rule_coupon_request'; + $login = $this->getLogin(); + /** @var Captcha $captcha */ + $captcha = $this->captchaHelper->getCaptcha($formId); + //Avoid logging multiple times or recalculating $required when the same codes are checked. + if (array_key_exists($code, $this->loggedFor)) { + $required = $this->loggedFor[$code]; + } else { + $required = $captcha->isRequired($login); + if (!$this->checkCode($code)) { + $captcha->logAttempt($login); + } + $this->loggedFor[$code] = $required; + } + + $value = null; + if ($required) { + $valid = false; + $value = $this->captchaResolver->resolve($this->request, $formId); + if ($value) { + if (array_key_exists($code, $this->checked) && array_key_exists($value, $this->checked[$code])) { + $valid = $this->checked[$code][$value]; + } else { + $valid = $captcha->isCorrect($value); + $this->checked[$code][$value] = $valid; + } + } + } else { + $valid = true; + } + + if (!$valid) { + if ($value) { + $message = __('Incorrect CAPTCHA'); + } else { + $message = __('Too many coupon code requests, please try again later.'); + } + throw new CodeRequestLimitException($message); + } + } +} diff --git a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php index bff74740aa241..b698190997d7e 100644 --- a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php +++ b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php @@ -5,6 +5,11 @@ */ namespace Magento\SalesRule\Model\Service; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\SalesRule\Api\CouponRepositoryInterface; + /** * Coupon management service class * @@ -14,6 +19,7 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement { /** * @var \Magento\SalesRule\Model\CouponFactory + * @deprecated */ protected $couponFactory; @@ -24,6 +30,7 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement /** * @var \Magento\SalesRule\Model\ResourceModel\Coupon\CollectionFactory + * @deprecated */ protected $collectionFactory; @@ -34,6 +41,7 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement /** * @var \Magento\SalesRule\Model\Spi\CouponResourceInterface + * @deprecated */ protected $resourceModel; @@ -42,6 +50,16 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement */ protected $couponMassDeleteResultFactory; + /** + * @var SearchCriteriaBuilder + */ + private $criteriaBuilder; + + /** + * @var CouponRepositoryInterface + */ + private $repository; + /** * @param \Magento\SalesRule\Model\CouponFactory $couponFactory * @param \Magento\SalesRule\Model\RuleFactory $ruleFactory @@ -49,6 +67,8 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement * @param \Magento\SalesRule\Model\Coupon\Massgenerator $couponGenerator * @param \Magento\SalesRule\Model\Spi\CouponResourceInterface $resourceModel * @param \Magento\SalesRule\Api\Data\CouponMassDeleteResultInterfaceFactory $couponMassDeleteResultFactory + * @param SearchCriteriaBuilder|null $criteriaBuilder + * @param CouponRepositoryInterface|null $repository */ public function __construct( \Magento\SalesRule\Model\CouponFactory $couponFactory, @@ -56,7 +76,9 @@ public function __construct( \Magento\SalesRule\Model\ResourceModel\Coupon\CollectionFactory $collectionFactory, \Magento\SalesRule\Model\Coupon\Massgenerator $couponGenerator, \Magento\SalesRule\Model\Spi\CouponResourceInterface $resourceModel, - \Magento\SalesRule\Api\Data\CouponMassDeleteResultInterfaceFactory $couponMassDeleteResultFactory + \Magento\SalesRule\Api\Data\CouponMassDeleteResultInterfaceFactory $couponMassDeleteResultFactory, + ?SearchCriteriaBuilder $criteriaBuilder = null, + ?CouponRepositoryInterface $repository = null ) { $this->couponFactory = $couponFactory; $this->ruleFactory = $ruleFactory; @@ -64,6 +86,8 @@ public function __construct( $this->couponGenerator = $couponGenerator; $this->resourceModel = $resourceModel; $this->couponMassDeleteResultFactory = $couponMassDeleteResultFactory; + $this->criteriaBuilder = $criteriaBuilder ?? ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); + $this->repository = $repository ?? ObjectManager::getInstance()->get(CouponRepositoryInterface::class); } /** @@ -84,7 +108,7 @@ public function generate(\Magento\SalesRule\Api\Data\CouponGenerationSpecInterfa try { $rule = $this->ruleFactory->create()->load($couponSpec->getRuleId()); if (!$rule->getRuleId()) { - throw \Magento\Framework\Exception\NoSuchEntityException::singleField( + throw NoSuchEntityException::singleField( \Magento\SalesRule\Model\Coupon::KEY_RULE_ID, $couponSpec->getRuleId() ); @@ -156,7 +180,7 @@ public function deleteByIds(array $ids, $ignoreInvalidCoupons = true) /** * Delete coupon by coupon codes. * - * @param string[] codes + * @param string[] $codes * @param bool $ignoreInvalidCoupons * @return \Magento\SalesRule\Api\Data\CouponMassDeleteResultInterface * @throws \Magento\Framework\Exception\LocalizedException @@ -170,21 +194,18 @@ public function deleteByCodes(array $codes, $ignoreInvalidCoupons = true) * Delete coupons by filter * * @param string $fieldName - * @param string[] fieldValues + * @param string[] $fieldValues * @param bool $ignoreInvalid * @return \Magento\SalesRule\Api\Data\CouponMassDeleteResultInterface * @throws \Magento\Framework\Exception\LocalizedException */ protected function massDelete($fieldName, array $fieldValues, $ignoreInvalid) { - $couponsCollection = $this->collectionFactory->create() - ->addFieldToFilter( - $fieldName, - ['in' => $fieldValues] - ); + $this->criteriaBuilder->addFilter($fieldName, $fieldValues, 'in'); + $couponsCollection = $this->repository->getList($this->criteriaBuilder->create()); if (!$ignoreInvalid) { - if ($couponsCollection->getSize() != count($fieldValues)) { + if ($couponsCollection->getTotalCount() != count($fieldValues)) { throw new \Magento\Framework\Exception\LocalizedException(__('Some coupons are invalid.')); } } @@ -192,11 +213,10 @@ protected function massDelete($fieldName, array $fieldValues, $ignoreInvalid) $results = $this->couponMassDeleteResultFactory->create(); $failedItems = []; $fieldValues = array_flip($fieldValues); - /** @var \Magento\SalesRule\Model\Coupon $coupon */ foreach ($couponsCollection->getItems() as $coupon) { $couponValue = ($fieldName == 'code') ? $coupon->getCode() : $coupon->getCouponId(); try { - $coupon->delete(); + $this->repository->deleteById($coupon->getCouponId()); } catch (\Exception $e) { $failedItems[] = $couponValue; } diff --git a/app/code/Magento/SalesRule/Model/Spi/CodeLimitManagerInterface.php b/app/code/Magento/SalesRule/Model/Spi/CodeLimitManagerInterface.php new file mode 100644 index 0000000000000..dfee9e24a83fe --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Spi/CodeLimitManagerInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Spi; + +use Magento\SalesRule\Api\Exception\CodeRequestLimitException; + +/** + * Determine whether number of requests for coupon codes has reached a limit. + */ +interface CodeLimitManagerInterface +{ + /** + * Checks whether the request for a code was issued after reaching a limit. + * + * @param string $code + * @throws CodeRequestLimitException If a limit has been reached. + * @return void + */ + public function checkRequest(string $code): void; +} diff --git a/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php b/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php new file mode 100644 index 0000000000000..02fd81078ea7c --- /dev/null +++ b/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Observer; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Event\Observer as EventObserver; +use Magento\Framework\Event\ObserverInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote; +use Magento\SalesRule\Api\Exception\CodeRequestLimitException; +use Magento\SalesRule\Model\Spi\CodeLimitManagerInterface; + +/** + * Validate newly provided coupon code before using it while calculating totals. + */ +class CouponCodeValidation implements ObserverInterface +{ + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $criteriaBuilder; + + /** + * @var CodeLimitManagerInterface + */ + private $codeLimitManager; + + /** + * @param CodeLimitManagerInterface $codeLimitManager + * @param CartRepositoryInterface $cartRepository + * @param SearchCriteriaBuilder $criteriaBuilder + */ + public function __construct( + CodeLimitManagerInterface $codeLimitManager, + CartRepositoryInterface $cartRepository, + SearchCriteriaBuilder $criteriaBuilder + ) { + $this->codeLimitManager = $codeLimitManager; + $this->cartRepository = $cartRepository; + $this->criteriaBuilder = $criteriaBuilder; + } + + /** + * @inheritDoc + */ + public function execute(EventObserver $observer) + { + /** @var Quote $quote */ + $quote = $observer->getData('quote'); + $code = $quote->getCouponCode(); + if ($code) { + //Only validating the code if it's a new code. + /** @var Quote[] $found */ + $found = $this->cartRepository->getList( + $this->criteriaBuilder->addFilter('main_table.' . CartInterface::KEY_ENTITY_ID, $quote->getId()) + ->create() + )->getItems(); + if (!$found || ((string)array_shift($found)->getCouponCode()) !== (string)$code) { + try { + $this->codeLimitManager->checkRequest($code); + } catch (CodeRequestLimitException $exception) { + $quote->setCouponCode(''); + throw $exception; + } + } + } + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Service/CouponManagementServiceTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Service/CouponManagementServiceTest.php deleted file mode 100644 index d68df19e466e2..0000000000000 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Service/CouponManagementServiceTest.php +++ /dev/null @@ -1,353 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\SalesRule\Test\Unit\Model\Service; - -/** - * Class CouponManagementServiceTest - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class CouponManagementServiceTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\SalesRule\Model\Service\CouponManagementService - */ - protected $model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $couponFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $ruleFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $collectionFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $couponGenerator; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $resourceModel; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $couponMassDeleteResultFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $couponMassDeleteResult; - - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $objectManager; - - /** - * Setup the test - */ - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $className = \Magento\SalesRule\Model\CouponFactory::class; - $this->couponFactory = $this->createMock($className); - - $className = \Magento\SalesRule\Model\RuleFactory::class; - $this->ruleFactory = $this->createPartialMock($className, ['create']); - - $className = \Magento\SalesRule\Model\ResourceModel\Coupon\CollectionFactory::class; - $this->collectionFactory = $this->createPartialMock($className, ['create']); - - $className = \Magento\SalesRule\Model\Coupon\Massgenerator::class; - $this->couponGenerator = $this->createMock($className); - - $className = \Magento\SalesRule\Model\Spi\CouponResourceInterface::class; - $this->resourceModel = $this->createMock($className); - - $className = \Magento\SalesRule\Api\Data\CouponMassDeleteResultInterface::class; - $this->couponMassDeleteResult = $this->createMock($className); - - $className = \Magento\SalesRule\Api\Data\CouponMassDeleteResultInterfaceFactory::class; - $this->couponMassDeleteResultFactory = $this->createPartialMock($className, ['create']); - $this->couponMassDeleteResultFactory - ->expects($this->any()) - ->method('create') - ->willReturn($this->couponMassDeleteResult); - - $this->model = $this->objectManager->getObject( - \Magento\SalesRule\Model\Service\CouponManagementService::class, - [ - 'couponFactory' => $this->couponFactory, - 'ruleFactory' => $this->ruleFactory, - 'collectionFactory' => $this->collectionFactory, - 'couponGenerator' => $this->couponGenerator, - 'resourceModel' => $this->resourceModel, - 'couponMassDeleteResultFactory' => $this->couponMassDeleteResultFactory, - ] - ); - } - - /** - * test Generate - */ - public function testGenerate() - { - $className = \Magento\SalesRule\Model\Data\CouponGenerationSpec::class; - /** - * @var \Magento\SalesRule\Api\Data\CouponGenerationSpecInterface $couponSpec - */ - $couponSpec = $this->createPartialMock( - $className, - ['getRuleId', 'getQuantity', 'getFormat', 'getLength', 'setData'] - ); - - $couponSpec->expects($this->atLeastOnce())->method('getRuleId')->willReturn(1); - $couponSpec->expects($this->once())->method('getQuantity')->willReturn(1); - $couponSpec->expects($this->once())->method('getFormat')->willReturn('num'); - $couponSpec->expects($this->once())->method('getLength')->willReturn(1); - - $this->couponGenerator->expects($this->atLeastOnce())->method('setData'); - $this->couponGenerator->expects($this->once())->method('validateData')->willReturn(true); - $this->couponGenerator->expects($this->once())->method('generatePool'); - $this->couponGenerator->expects($this->once())->method('getGeneratedCodes')->willReturn([]); - - /** - * @var \Magento\SalesRule\Model\Rule $rule - */ - $rule = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, - ['load', 'getRuleId', 'getToDate', 'getUsesPerCoupon', 'getUsesPerCustomer', 'getUseAutoGeneration'] - ); - - $rule->expects($this->any())->method('load')->willReturnSelf(); - $rule->expects($this->any())->method('getRuleId')->willReturn(1); - $rule->expects($this->any())->method('getToDate')->willReturn('2015-07-31 00:00:00'); - $rule->expects($this->any())->method('getUsesPerCoupon')->willReturn(20); - $rule->expects($this->any())->method('getUsesPerCustomer')->willReturn(5); - $rule->expects($this->any())->method('getUseAutoGeneration')->willReturn(true); - - $this->ruleFactory->expects($this->any())->method('create')->willReturn($rule); - - $result = $this->model->generate($couponSpec); - $this->assertEquals([], $result); - } - - /** - * test Generate with validation Exception - * @throws \Magento\Framework\Exception\InputException - */ - public function testGenerateValidationException() - { - $className = \Magento\SalesRule\Api\Data\CouponGenerationSpecInterface::class; - /** - * @var \Magento\SalesRule\Api\Data\CouponGenerationSpecInterface $couponSpec - */ - $couponSpec = $this->createMock($className); - - /** - * @var \Magento\SalesRule\Model\Rule $rule - */ - $rule = $this->createPartialMock(\Magento\SalesRule\Model\Rule::class, ['load', 'getRuleId']); - - $rule->expects($this->any())->method('load')->willReturnSelf(); - $rule->expects($this->any())->method('getRuleId')->willReturn(1); - - $this->ruleFactory->expects($this->any())->method('create')->willReturn($rule); - - $this->couponGenerator->expects($this->once())->method('validateData') - ->willThrowException(new \Magento\Framework\Exception\InputException()); - $this->expectException(\Magento\Framework\Exception\InputException::class); - - $this->model->generate($couponSpec); - } - - /** - * test Generate with localized Exception - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function testGenerateLocalizedException() - { - $className = \Magento\SalesRule\Api\Data\CouponGenerationSpecInterface::class; - /** - * @var \Magento\SalesRule\Api\Data\CouponGenerationSpecInterface $couponSpec - */ - $couponSpec = $this->createMock($className); - - /** - * @var \Magento\SalesRule\Model\Rule $rule - */ - $rule = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, - ['load', 'getRuleId', 'getUseAutoGeneration'] - ); - $rule->expects($this->any())->method('load')->willReturnSelf(); - $rule->expects($this->any())->method('getRuleId')->willReturn(1); - $rule->expects($this->once())->method('getUseAutoGeneration') - ->willThrowException( - new \Magento\Framework\Exception\LocalizedException( - __('Error occurred when generating coupons: %1', '1') - ) - ); - $this->ruleFactory->expects($this->any())->method('create')->willReturn($rule); - - $this->couponGenerator->expects($this->once())->method('validateData')->willReturn(true); - - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - - $this->model->generate($couponSpec); - } - - /** - * test Generate with localized Exception - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function testGenerateNoSuchEntity() - { - $className = \Magento\SalesRule\Api\Data\CouponGenerationSpecInterface::class; - /** - * @var \Magento\SalesRule\Api\Data\CouponGenerationSpecInterface $couponSpec - */ - $couponSpec = $this->createMock($className); - - /** - * @var \Magento\SalesRule\Model\Rule $rule - */ - $rule = $this->createPartialMock(\Magento\SalesRule\Model\Rule::class, ['load', 'getRuleId']); - - $rule->expects($this->any())->method('load')->willReturnSelf(); - $rule->expects($this->any())->method('getRuleId')->willReturn(false); - - $this->ruleFactory->expects($this->any())->method('create')->willReturn($rule); - - $this->couponGenerator->expects($this->once())->method('validateData')->willReturn(true); - - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - - $this->model->generate($couponSpec); - } - - /** - * test DeleteByIds with Ignore non existing - */ - public function testDeleteByIdsIgnore() - { - $ids = [1, 2, 3]; - - $className = \Magento\SalesRule\Model\Coupon::class; - /** - * @var \Magento\SalesRule\Model\Coupon $coupon - */ - $coupon = $this->createMock($className); - $coupon->expects($this->exactly(3))->method('delete'); - - $className = \Magento\SalesRule\Model\ResourceModel\Coupon\Collection::class; - /** - * @var \Magento\SalesRule\Model\ResourceModel\Coupon\Collection $couponCollection - */ - $couponCollection = $this->createMock($className); - - $couponCollection->expects($this->once())->method('addFieldToFilter')->willReturnSelf(); - $couponCollection->expects($this->once())->method('getItems')->willReturn([$coupon, $coupon, $coupon]); - $this->collectionFactory->expects($this->once())->method('create')->willReturn($couponCollection); - - $this->couponMassDeleteResult->expects($this->once())->method('setFailedItems')->willReturnSelf(); - $this->couponMassDeleteResult->expects($this->once())->method('setMissingItems')->willReturnSelf(); - - $this->model->deleteByIds($ids, true); - } - - /** - * test DeleteByIds with not Ignore non existing - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function testDeleteByAnyNoIgnore() - { - $ids = [1, 2, 3]; - - $className = \Magento\SalesRule\Model\ResourceModel\Coupon\Collection::class; - /** - * @var \Magento\SalesRule\Model\ResourceModel\Coupon\Collection $couponCollection - */ - $couponCollection = $this->createMock($className); - $couponCollection->expects($this->once())->method('addFieldToFilter')->willReturnSelf(); - $this->collectionFactory->expects($this->once())->method('create')->willReturn($couponCollection); - - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - - $this->model->deleteByIds($ids, false); - } - - /** - * test DeleteByIds with not Ignore non existing - */ - public function testDeleteByAnyExceptionOnDelete() - { - $ids = [1, 2, 3]; - - /** - * @var \Magento\SalesRule\Model\Coupon $coupon - */ - $className = \Magento\SalesRule\Model\Coupon::class; - $coupon = $this->createMock($className); - $coupon->expects($this->any())->method('delete')->willThrowException(new \Exception()); - - /** - * @var \Magento\SalesRule\Model\ResourceModel\Coupon\Collection $couponCollection - */ - $className = \Magento\SalesRule\Model\ResourceModel\Coupon\Collection::class; - $couponCollection = $this->createMock($className); - $couponCollection->expects($this->once())->method('addFieldToFilter')->willReturnSelf(); - $couponCollection->expects($this->once())->method('getItems')->willReturn([$coupon, $coupon, $coupon]); - $this->collectionFactory->expects($this->once())->method('create')->willReturn($couponCollection); - - $this->couponMassDeleteResult->expects($this->once())->method('setFailedItems')->willReturnSelf(); - $this->couponMassDeleteResult->expects($this->once())->method('setMissingItems')->willReturnSelf(); - - $this->model->deleteByIds($ids, true); - } - - /** - * test DeleteByCodes - */ - public function testDeleteByCodes() - { - $ids = [1, 2, 3]; - - $className = \Magento\SalesRule\Model\Coupon::class; - /** - * @var \Magento\SalesRule\Model\Coupon $coupon - */ - $coupon = $this->createMock($className); - $coupon->expects($this->exactly(3))->method('delete'); - - $className = \Magento\SalesRule\Model\ResourceModel\Coupon\Collection::class; - /** - * @var \Magento\SalesRule\Model\ResourceModel\Coupon\Collection $couponCollection - */ - $couponCollection = $this->createMock($className); - - $couponCollection->expects($this->once())->method('addFieldToFilter')->willReturnSelf(); - $couponCollection->expects($this->once())->method('getItems')->willReturn([$coupon, $coupon, $coupon]); - $this->collectionFactory->expects($this->once())->method('create')->willReturn($couponCollection); - - $this->couponMassDeleteResult->expects($this->once())->method('setFailedItems')->willReturnSelf(); - $this->couponMassDeleteResult->expects($this->once())->method('setMissingItems')->willReturnSelf(); - - $this->model->deleteByCodes($ids, true); - } -} diff --git a/app/code/Magento/SalesRule/composer.json b/app/code/Magento/SalesRule/composer.json index a2e7dc8835ae7..bb51ef8dca685 100644 --- a/app/code/Magento/SalesRule/composer.json +++ b/app/code/Magento/SalesRule/composer.json @@ -22,7 +22,10 @@ "magento/module-shipping": "*", "magento/module-store": "*", "magento/module-ui": "*", - "magento/module-widget": "*" + "magento/module-widget": "*", + "magento/module-captcha": "*", + "magento/module-checkout": "*", + "magento/module-authorization": "*" }, "suggest": { "magento/module-sales-rule-sample-data": "*" diff --git a/app/code/Magento/SalesRule/etc/adminhtml/di.xml b/app/code/Magento/SalesRule/etc/adminhtml/di.xml index bd0fb8ac1b14e..3b8fae469ffa9 100644 --- a/app/code/Magento/SalesRule/etc/adminhtml/di.xml +++ b/app/code/Magento/SalesRule/etc/adminhtml/di.xml @@ -18,4 +18,8 @@ </argument> </arguments> </type> + + <preference + for="Magento\SalesRule\Model\Spi\CodeLimitManagerInterface" + type="Magento\SalesRule\Model\Coupon\AdminCodeLimitManager" /> </config> diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml index 863467ed3d318..c1d22a04771ab 100644 --- a/app/code/Magento/SalesRule/etc/di.xml +++ b/app/code/Magento/SalesRule/etc/di.xml @@ -186,4 +186,7 @@ <plugin name="coupon_uses_increment_plugin" type="Magento\SalesRule\Plugin\CouponUsagesIncrement" sortOrder="20"/> <plugin name="coupon_uses_decrement_plugin" type="Magento\SalesRule\Plugin\CouponUsagesDecrement" /> </type> + <preference + for="Magento\SalesRule\Model\Spi\CodeLimitManagerInterface" + type="Magento\SalesRule\Model\Coupon\CodeLimitManager" /> </config> diff --git a/app/code/Magento/SalesRule/etc/events.xml b/app/code/Magento/SalesRule/etc/events.xml index 8261860bbb7ce..fb0f711144e27 100644 --- a/app/code/Magento/SalesRule/etc/events.xml +++ b/app/code/Magento/SalesRule/etc/events.xml @@ -24,4 +24,7 @@ <event name="magento_salesrule_api_data_ruleinterface_load_after"> <observer name="legacy_model_load" instance="Magento\Framework\EntityManager\Observer\AfterEntityLoad" /> </event> + <event name="sales_quote_address_collect_totals_before"> + <observer name="coupon_code_validation" instance="Magento\SalesRule\Observer\CouponCodeValidation" /> + </event> </config> diff --git a/app/code/Magento/SalesRule/etc/frontend/di.xml b/app/code/Magento/SalesRule/etc/frontend/di.xml index bd0fb8ac1b14e..0b51ac2b566d9 100644 --- a/app/code/Magento/SalesRule/etc/frontend/di.xml +++ b/app/code/Magento/SalesRule/etc/frontend/di.xml @@ -18,4 +18,18 @@ </argument> </arguments> </type> + <type name="Magento\Checkout\Model\CompositeConfigProvider"> + <arguments> + <argument name="configProviders" xsi:type="array"> + <item name="checkout_sales_rule_captcha_config_provider" xsi:type="object">Magento\SalesRule\Model\Coupon\CaptchaConfigProvider</item> + </argument> + </arguments> + </type> + <type name="Magento\Captcha\CustomerData\Captcha"> + <arguments> + <argument name="formIds" xsi:type="array"> + <item name="sales_rule_coupon_request" xsi:type="string">sales_rule_coupon_request</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/SalesRule/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/SalesRule/view/frontend/layout/checkout_index_index.xml index f75525576f16b..b6b87682d9653 100644 --- a/app/code/Magento/SalesRule/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/SalesRule/view/frontend/layout/checkout_index_index.xml @@ -30,6 +30,12 @@ <item name="component" xsi:type="string">Magento_SalesRule/js/view/payment/discount-messages</item> <item name="displayArea" xsi:type="string">messages</item> </item> + <item name="captcha" xsi:type="array"> + <item name="component" xsi:type="string">Magento_SalesRule/js/view/payment/captcha</item> + <item name="displayArea" xsi:type="string">captcha</item> + <item name="formId" xsi:type="string">sales_rule_coupon_request</item> + <item name="configSource" xsi:type="string">checkoutConfig</item> + </item> </item> </item> </item> diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/action/cancel-coupon.js b/app/code/Magento/SalesRule/view/frontend/web/js/action/cancel-coupon.js index ed77cf188413c..72eec1be0766c 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/js/action/cancel-coupon.js +++ b/app/code/Magento/SalesRule/view/frontend/web/js/action/cancel-coupon.js @@ -22,7 +22,26 @@ define([ ) { 'use strict'; - return function (isApplied) { + var successCallbacks = [], + action, + callSuccessCallbacks; + + /** + * Execute callbacks when a coupon is successfully canceled. + */ + callSuccessCallbacks = function () { + successCallbacks.forEach(function (callback) { + callback(); + }); + }; + + /** + * Cancel applied coupon. + * + * @param {Boolean} isApplied + * @returns {Deferred} + */ + action = function (isApplied) { var quoteId = quote.getQuoteId(), url = urlManager.getCancelCouponUrl(quoteId), message = $t('Your coupon was successfully removed.'); @@ -42,6 +61,8 @@ define([ isApplied(false); totals.isLoading(false); fullScreenLoader.stopLoader(); + //Allowing to tap into coupon-cancel process. + callSuccessCallbacks(); }); messageContainer.addSuccessMessage({ 'message': message @@ -52,4 +73,15 @@ define([ errorProcessor.process(response, messageContainer); }); }; + + /** + * Callback for when the cancel-coupon process is finished. + * + * @param {Function} callback + */ + action.registerSuccessCallback = function (callback) { + successCallbacks.push(callback); + }; + + return action; }); diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/action/set-coupon-code.js b/app/code/Magento/SalesRule/view/frontend/web/js/action/set-coupon-code.js index 4d29d95e6777c..994ccf2b395d2 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/js/action/set-coupon-code.js +++ b/app/code/Magento/SalesRule/view/frontend/web/js/action/set-coupon-code.js @@ -23,17 +23,37 @@ define([ ) { 'use strict'; - return function (couponCode, isApplied) { + var dataModifiers = [], + successCallbacks = [], + failCallbacks = [], + action; + + /** + * Apply provided coupon. + * + * @param {String} couponCode + * @param {Boolean}isApplied + * @returns {Deferred} + */ + action = function (couponCode, isApplied) { var quoteId = quote.getQuoteId(), url = urlManager.getApplyCouponUrl(couponCode, quoteId), - message = $t('Your coupon was successfully applied.'); + message = $t('Your coupon was successfully applied.'), + data = {}, + headers = {}; + //Allowing to modify coupon-apply request + dataModifiers.forEach(function (modifier) { + modifier(headers, data); + }); fullScreenLoader.startLoader(); return storage.put( url, - {}, - false + data, + false, + null, + headers ).done(function (response) { var deferred; @@ -50,11 +70,48 @@ define([ messageContainer.addSuccessMessage({ 'message': message }); + //Allowing to tap into apply-coupon process. + successCallbacks.forEach(function (callback) { + callback(response); + }); } }).fail(function (response) { fullScreenLoader.stopLoader(); totals.isLoading(false); errorProcessor.process(response, messageContainer); + //Allowing to tap into apply-coupon process. + failCallbacks.forEach(function (callback) { + callback(response); + }); }); }; + + /** + * Modifying data to be sent. + * + * @param {Function} modifier + */ + action.registerDataModifier = function (modifier) { + dataModifiers.push(modifier); + }; + + /** + * When successfully added a coupon. + * + * @param {Function} callback + */ + action.registerSuccessCallback = function (callback) { + successCallbacks.push(callback); + }; + + /** + * When failed to add a coupon. + * + * @param {Function} callback + */ + action.registerFailCallback = function (callback) { + failCallbacks.push(callback); + }; + + return action; }); diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/view/payment/captcha.js b/app/code/Magento/SalesRule/view/frontend/web/js/view/payment/captcha.js new file mode 100644 index 0000000000000..f289377e48294 --- /dev/null +++ b/app/code/Magento/SalesRule/view/frontend/web/js/view/payment/captcha.js @@ -0,0 +1,68 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'Magento_SalesRule/js/action/set-coupon-code', + 'Magento_SalesRule/js/action/cancel-coupon', + 'Magento_Checkout/js/model/quote', + 'ko' + ], + function (defaultCaptcha, captchaList, setCouponCodeAction, cancelCouponAction, quote, ko) { + 'use strict'; + + var totals = quote.getTotals(), + couponCode = ko.observable(null), + isApplied; + + if (totals()) { + couponCode(totals()['coupon_code']); + } + //Captcha can only be required for adding a coupon so we need to know if one was added already. + isApplied = ko.observable(couponCode() != null); + + return defaultCaptcha.extend({ + /** @inheritdoc */ + initialize: function () { + var self = this, + currentCaptcha; + + this._super(); + //Getting coupon captcha model. + currentCaptcha = captchaList.getCaptchaByFormId(this.formId); + + if (currentCaptcha != null) { + if (!isApplied()) { + //Show captcha if we don't have a coupon applied. + currentCaptcha.setIsVisible(true); + } + this.setCurrentCaptcha(currentCaptcha); + //Add captcha code to coupon-apply request. + setCouponCodeAction.registerDataModifier(function (headers) { + if (self.isRequired()) { + headers['X-Captcha'] = self.captchaValue()(); + } + }); + //Refresh captcha after failed request. + setCouponCodeAction.registerFailCallback(function () { + if (self.isRequired()) { + self.refresh(); + } + }); + //Hide captcha when a coupon has been applied. + setCouponCodeAction.registerSuccessCallback(function () { + self.setIsVisible(false); + }); + //Show captcha again if it was canceled. + cancelCouponAction.registerSuccessCallback(function () { + if (self.isRequired()) { + self.setIsVisible(true); + } + }); + } + } + }); + }); diff --git a/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html b/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html index d622b5ea5762d..649c1ba4bfe59 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html +++ b/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html @@ -45,6 +45,9 @@ <!-- /ko --> </div> </div> + <!-- ko foreach: getRegion('captcha') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!-- /ko --> </form> </div> </div> diff --git a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php index 1ac1547eb8d0a..d589498cdaa3e 100644 --- a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php +++ b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php @@ -85,6 +85,7 @@ protected function _construct() */ private function queryByPhrase($phrase) { + $phrase = $this->fullTextSelect->removeSpecialCharacters($phrase); $matchQuery = $this->fullTextSelect->getMatchQuery( ['synonyms' => 'synonyms'], $this->escapePhrase($phrase), diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index d0beac8697f81..95877c626256e 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -550,10 +550,10 @@ protected function _getSitemapRow($url, $lastmod = null, $changefreq = null, $pr $row .= '<lastmod>' . $this->_getFormattedLastmodDate($lastmod) . '</lastmod>'; } if ($changefreq) { - $row .= '<changefreq>' . $changefreq . '</changefreq>'; + $row .= '<changefreq>' . $this->_escaper->escapeHtml($changefreq) . '</changefreq>'; } if ($priority) { - $row .= sprintf('<priority>%.1f</priority>', $priority); + $row .= sprintf('<priority>%.1f</priority>', $this->_escaper->escapeHtml($priority)); } if ($images) { // Add Images to sitemap diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php index 86d57815e02be..1becde2eb3498 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php @@ -87,7 +87,7 @@ class SitemapTest extends \PHPUnit\Framework\TestCase private $configReaderMock; /** - * Set helper mocks, create resource model mock + * @inheritdoc */ protected function setUp() { @@ -604,9 +604,8 @@ private function getModelConstructorArgs() ->getMockForAbstractClass(); $objectManager = new ObjectManager($this); - $escaper = $objectManager->getObject( - \Magento\Framework\Escaper::class - ); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $constructArguments = $objectManager->getConstructArguments( Sitemap::class, [ diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index f62762986cb32..dab9c55c216d9 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -423,14 +423,9 @@ public function __construct( /** * @inheritdoc - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $properties = parent::__sleep(); $properties = array_diff($properties, ['_coreFileStorageDatabase', '_config']); return $properties; @@ -440,14 +435,9 @@ public function __sleep() * Init not serializable fields * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $this->_coreFileStorageDatabase = ObjectManager::getInstance() ->get(\Magento\MediaStorage\Helper\File\Storage\Database::class); @@ -727,6 +717,7 @@ protected function _updatePathUseRewrites($url) $indexFileName = 'index.php'; } else { $scriptFilename = $this->_request->getServer('SCRIPT_FILENAME'); + // phpcs:ignore Magento2.Functions.DiscouragedFunction $indexFileName = basename($scriptFilename); } $url .= $indexFileName . '/'; @@ -1081,9 +1072,10 @@ public function getWebsiteId() /** * Reinit Stores on after save * - * @deprecated 100.1.3 * @return $this + * @throws \Exception * @since 100.1.3 + * @deprecated 100.1.3 */ public function afterSave() { @@ -1094,9 +1086,11 @@ public function afterSave() $event = $this->_eventPrefix . '_edit'; } $store = $this; - $this->getResource()->addCommitCallback(function () use ($event, $store) { - $this->eventManager->dispatch($event, ['store' => $store]); - }); + $this->getResource()->addCommitCallback( + function () use ($event, $store) { + $this->eventManager->dispatch($event, ['store' => $store]); + } + ); $this->pillPut->put(); return parent::afterSave(); } @@ -1214,10 +1208,12 @@ public function getCurrentUrl($fromStore = true) return $storeUrl; } + // phpcs:ignore Magento2.Functions.DiscouragedFunction $storeParsedUrl = parse_url($storeUrl); $storeParsedQuery = []; if (isset($storeParsedUrl['query'])) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction parse_str($storeParsedUrl['query'], $storeParsedQuery); } @@ -1245,6 +1241,7 @@ public function getCurrentUrl($fromStore = true) $requestStringParts = explode('?', $requestString, 2); $requestStringPath = $requestStringParts[0]; if (isset($requestStringParts[1])) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction parse_str($requestStringParts[1], $requestString); } else { $requestString = []; @@ -1294,10 +1291,12 @@ public function beforeDelete() public function afterDelete() { $store = $this; - $this->getResource()->addCommitCallback(function () use ($store) { - $this->_storeManager->reinitStores(); - $this->eventManager->dispatch($this->_eventPrefix . '_delete', ['store' => $store]); - }); + $this->getResource()->addCommitCallback( + function () use ($store) { + $this->_storeManager->reinitStores(); + $this->eventManager->dispatch($this->_eventPrefix . '_delete', ['store' => $store]); + } + ); parent::afterDelete(); $this->_configCacheType->clean(); @@ -1388,6 +1387,7 @@ public function getIdentities() */ public function getStorePath() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $parsedUrl = parse_url($this->getBaseUrl()); return $parsedUrl['path'] ?? '/'; } diff --git a/app/code/Magento/Store/Test/Mftf/Suite/SecureStorefrontURLSuite.xml b/app/code/Magento/Store/Test/Mftf/Suite/SecureStorefrontURLSuite.xml new file mode 100644 index 0000000000000..798bdb1c12686 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Suite/SecureStorefrontURLSuite.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="SecureStorefrontURLSuite"> + <include> + <group name="secure_storefront_url"/> + </include> + </suite> +</suites> 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 b0ea10b1ed968..ef635c48e3466 100644 --- a/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css +++ b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css @@ -153,18 +153,6 @@ 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%; -} - .data-table .col-swatch-min-width input[type="text"] { padding: inherit; } diff --git a/app/code/Magento/Tax/Model/System/Message/Notifications.php b/app/code/Magento/Tax/Model/System/Message/Notifications.php index 5d274f0d2b1c9..ca59ab9eec3bf 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notifications.php +++ b/app/code/Magento/Tax/Model/System/Message/Notifications.php @@ -5,6 +5,8 @@ */ namespace Magento\Tax\Model\System\Message; +use Magento\Framework\App\ObjectManager; + /** * Notifications class */ @@ -53,22 +55,30 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface */ private $notifications = []; + /** + * @var \Magento\Framework\Escaper + */ + private $escaper; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\UrlInterface $urlBuilder * @param \Magento\Tax\Model\Config $taxConfig * @param NotificationInterface[] $notifications + * @param \Magento\Framework\Escaper|null $escaper */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\UrlInterface $urlBuilder, \Magento\Tax\Model\Config $taxConfig, - $notifications = [] + $notifications = [], + \Magento\Framework\Escaper $escaper = null ) { $this->storeManager = $storeManager; $this->urlBuilder = $urlBuilder; $this->taxConfig = $taxConfig; $this->notifications = $notifications; + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(\Magento\Framework\Escaper::class); } /** @@ -79,11 +89,12 @@ public function __construct( */ public function getIdentity() { + // phpcs:ignore Magento2.Security.InsecureFunction return md5('TAX_NOTIFICATION'); } /** - * {@inheritdoc} + * @inheritdoc */ public function isDisplayed() { @@ -96,7 +107,7 @@ public function isDisplayed() } /** - * {@inheritdoc} + * @inheritdoc */ public function getText() { @@ -135,7 +146,7 @@ public function getSeverity() */ public function getInfoUrl() { - return $this->taxConfig->getInfoUrl(); + return $this->escaper->escapeUrl($this->taxConfig->getInfoUrl()); } /** @@ -206,6 +217,7 @@ public function getIgnoreTaxNotificationUrl($section) /** * Return list of store names which have not compatible tax calculation type and price display settings. + * * Return true if settings are wrong for default store. * * @return array @@ -227,6 +239,7 @@ public function getStoresWithWrongDisplaySettings() /** * Return list of store names where tax discount settings are compatible. + * * Return true if settings are wrong for default store. * * @return array diff --git a/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php b/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php index 3fda67669fe86..a9fa12311803e 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/System/Message/NotificationsTest.php @@ -6,6 +6,7 @@ namespace Magento\Tax\Test\Unit\Model\System\Message; +use Magento\Framework\Escaper; use Magento\Tax\Model\Config as TaxConfig; use Magento\Tax\Model\System\Message\Notifications; use Magento\Store\Model\StoreManagerInterface; @@ -14,7 +15,7 @@ use Magento\Tax\Model\System\Message\NotificationInterface; /** - * Test class for @see \Magento\Tax\Model\System\Message\Notifications + * Test class for @see \Magento\Tax\Model\System\Message\Notifications. */ class NotificationsTest extends \PHPUnit\Framework\TestCase { @@ -43,21 +44,29 @@ class NotificationsTest extends \PHPUnit\Framework\TestCase */ private $notificationMock; + /** + * @var Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + + /** + * @inheritdoc + */ protected function setUp() { - parent::setUp(); - $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); $this->urlBuilderMock = $this->createMock(UrlInterface::class); $this->taxConfigMock = $this->createMock(TaxConfig::class); $this->notificationMock = $this->createMock(NotificationInterface::class); + $this->escaperMock = $this->createMock(Escaper::class); $this->notifications = (new ObjectManager($this))->getObject( Notifications::class, [ 'storeManager' => $this->storeManagerMock, 'urlBuilder' => $this->urlBuilderMock, 'taxConfig' => $this->taxConfigMock, - 'notifications' => [$this->notificationMock] + 'notifications' => [$this->notificationMock], + 'escaper' => $this->escaperMock, ] ); } @@ -84,12 +93,22 @@ public function dataProviderIsDisplayed() ]; } + /** + * Unit test for getText method. + * + * @return void + */ public function testGetText() { + $url = 'http://info-url'; $this->notificationMock->expects($this->once())->method('getText')->willReturn('Notification Text.'); - $this->taxConfigMock->expects($this->once())->method('getInfoUrl')->willReturn('http://info-url'); + $this->taxConfigMock->expects($this->once())->method('getInfoUrl')->willReturn($url); $this->urlBuilderMock->expects($this->once())->method('getUrl') ->with('adminhtml/system_config/edit/section/tax')->willReturn('http://tax-config-url'); + $this->escaperMock->expects($this->once()) + ->method('escapeUrl') + ->with($url) + ->willReturn($url); $this->assertEquals( 'Notification Text.<p>Please see <a href="http://info-url">documentation</a> for more details. ' @@ -97,4 +116,21 @@ public function testGetText() $this->notifications->getText() ); } + + /** + * Unit test for getInfoUrl method. + * + * @return void + */ + public function testGetInfoUrl() + { + $url = 'http://info-url'; + $this->taxConfigMock->expects($this->once())->method('getInfoUrl')->willReturn($url); + $this->escaperMock->expects($this->once()) + ->method('escapeUrl') + ->with($url) + ->willReturn($url); + + $this->assertEquals($url, $this->notifications->getInfoUrl()); + } } diff --git a/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php b/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php index ff5f02a0de5c5..317ab39d307cd 100644 --- a/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php +++ b/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php @@ -16,7 +16,7 @@ */ class JsFooterPlugin { - private const XML_PATH_DEV_MOVE_JS_TO_BOTTOM = 'dev/js/move_inline_to_bottom'; + private const XML_PATH_DEV_MOVE_JS_TO_BOTTOM = 'dev/js/move_script_to_bottom'; /** * @var ScopeConfigInterface diff --git a/app/code/Magento/Theme/etc/adminhtml/system.xml b/app/code/Magento/Theme/etc/adminhtml/system.xml index db92aee16c2bb..20500619354e0 100644 --- a/app/code/Magento/Theme/etc/adminhtml/system.xml +++ b/app/code/Magento/Theme/etc/adminhtml/system.xml @@ -7,7 +7,13 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> <system> - <section id="dev"> + <section id="dev" translate="label" type="text" sortOrder="920" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="js"> + <field id="move_script_to_bottom" translate="label" type="select" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Move JS code to the bottom of the page</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + </group> <group id="css"> <field id="use_css_critical_path" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Use CSS critical path</label> diff --git a/app/code/Magento/Theme/etc/config.xml b/app/code/Magento/Theme/etc/config.xml index 3e26204d7788c..1515c357e094e 100644 --- a/app/code/Magento/Theme/etc/config.xml +++ b/app/code/Magento/Theme/etc/config.xml @@ -67,7 +67,7 @@ Disallow: /*SID= <sign>1</sign> </static> <js> - <move_inline_to_bottom>0</move_inline_to_bottom> + <move_script_to_bottom>0</move_script_to_bottom> </js> <css> <use_css_critical_path>0</use_css_critical_path> diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/util/JSON.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/util/JSON.js index 56886fa50cca6..ccde4b8bc38e9 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/util/JSON.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/util/JSON.js @@ -92,7 +92,7 @@ */ parse: function(s) { try { - return eval('(' + s + ')'); + return JSON.parse(s); } catch (ex) { // Ignore } diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_jquery_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_jquery_src.js index 60dd358414be1..9eddd0f0c3e68 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_jquery_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_jquery_src.js @@ -1193,7 +1193,7 @@ tinymce.create('tinymce.util.Dispatcher', { parse: function(s) { try { - return eval('(' + s + ')'); + return JSON.parse(s); } catch (ex) { // Ignore } diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_prototype_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_prototype_src.js index ed0b7cb0e50a2..9230c2276c3e2 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_prototype_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_prototype_src.js @@ -945,7 +945,7 @@ tinymce.create('tinymce.util.Dispatcher', { parse: function(s) { try { - return eval('(' + s + ')'); + return JSON.parse(s); } catch (ex) { // Ignore } diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_src.js index 1c53062dd9690..88b60001a88d3 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/tiny_mce_src.js @@ -918,7 +918,7 @@ tinymce.create('tinymce.util.Dispatcher', { parse: function(s) { try { - return eval('(' + s + ')'); + return JSON.parse(s); } catch (ex) { // Ignore } diff --git a/app/code/Magento/Ui/Controller/Index/Render.php b/app/code/Magento/Ui/Controller/Index/Render.php index 6dc7e8b12db21..faab203547064 100644 --- a/app/code/Magento/Ui/Controller/Index/Render.php +++ b/app/code/Magento/Ui/Controller/Index/Render.php @@ -6,11 +6,20 @@ namespace Magento\Ui\Controller\Index; use Magento\Backend\App\Action\Context; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\View\Element\UiComponentInterface; +use Magento\Ui\Model\UiComponentTypeResolver; +use Magento\Framework\Escaper; +use Magento\Framework\Controller\Result\JsonFactory; +use Psr\Log\LoggerInterface; +use Magento\Framework\AuthorizationInterface; /** - * Is responsible for providing ui components information on store front + * Is responsible for providing ui components information on store front. + * + * @SuppressWarnings(PHPMD.AllPurposeAction) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Render extends \Magento\Framework\App\Action\Action { @@ -24,33 +33,124 @@ class Render extends \Magento\Framework\App\Action\Action */ private $uiComponentFactory; + /** + * @var UiComponentTypeResolver + */ + private $contentTypeResolver; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var Escaper + */ + private $escaper; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * Render constructor. * @param Context $context * @param UiComponentFactory $uiComponentFactory + * @param UiComponentTypeResolver|null $contentTypeResolver + * @param JsonFactory|null $resultJsonFactory + * @param Escaper|null $escaper + * @param LoggerInterface|null $logger */ - public function __construct(Context $context, UiComponentFactory $uiComponentFactory) - { + public function __construct( + Context $context, + UiComponentFactory $uiComponentFactory, + ?UiComponentTypeResolver $contentTypeResolver = null, + JsonFactory $resultJsonFactory = null, + Escaper $escaper = null, + LoggerInterface $logger = null + ) { parent::__construct($context); $this->context = $context; $this->uiComponentFactory = $uiComponentFactory; + $this->authorization = $context->getAuthorization(); + $this->contentTypeResolver = $contentTypeResolver + ?? ObjectManager::getInstance()->get(UiComponentTypeResolver::class); + $this->resultJsonFactory = $resultJsonFactory ?? ObjectManager::getInstance()->get(JsonFactory::class); + $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); + $this->logger = $logger ?? ObjectManager::getInstance()->get(LoggerInterface::class); } /** - * Action for AJAX request - * - * @return void + * @inheritdoc */ public function execute() { if ($this->_request->getParam('namespace') === null) { - $this->_redirect('noroute'); + $this->_redirect('admin/noroute'); + return; } - $component = $this->uiComponentFactory->create($this->_request->getParam('namespace')); - $this->prepareComponent($component); - $this->_response->appendBody((string) $component->render()); + try { + $component = $this->uiComponentFactory->create($this->getRequest()->getParam('namespace')); + if ($this->validateAclResource($component->getContext()->getDataProvider()->getConfigData())) { + $this->prepareComponent($component); + $this->getResponse()->appendBody((string)$component->render()); + + $contentType = $this->contentTypeResolver->resolve($component->getContext()); + $this->getResponse()->setHeader('Content-Type', $contentType, true); + } else { + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setStatusHeader( + \Zend\Http\Response::STATUS_CODE_403, + \Zend\Http\AbstractMessage::VERSION_11, + 'Forbidden' + ); + return $resultJson->setData( + [ + 'error' => $this->escaper->escapeHtml('Forbidden'), + 'errorcode' => 403 + ] + ); + } + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->logger->critical($e); + $result = [ + 'error' => $this->escaper->escapeHtml($e->getMessage()), + 'errorcode' => $this->escaper->escapeHtml($e->getCode()) + ]; + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setStatusHeader( + \Zend\Http\Response::STATUS_CODE_400, + \Zend\Http\AbstractMessage::VERSION_11, + 'Bad Request' + ); + + return $resultJson->setData($result); + } catch (\Exception $e) { + $this->logger->critical($e); + $result = [ + 'error' => __('UI component could not be rendered because of system exception'), + 'errorcode' => $this->escaper->escapeHtml($e->getCode()) + ]; + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setStatusHeader( + \Zend\Http\Response::STATUS_CODE_400, + \Zend\Http\AbstractMessage::VERSION_11, + 'Bad Request' + ); + + return $resultJson->setData($result); + } } /** @@ -66,4 +166,25 @@ private function prepareComponent(UiComponentInterface $component) } $component->prepare(); } + + /** + * Optionally validate ACL resource of components with a DataSource/DataProvider + * + * @param mixed $dataProviderConfigData + * @return bool + */ + private function validateAclResource($dataProviderConfigData) + { + if (isset($dataProviderConfigData['aclResource'])) { + if (!$this->authorization->isAllowed($dataProviderConfigData['aclResource'])) { + if (!$this->_request->isAjax()) { + $this->_redirect('noroute'); + } + + return false; + } + } + + return true; + } } diff --git a/app/code/Magento/Ui/Test/Unit/Controller/Index/RenderTest.php b/app/code/Magento/Ui/Test/Unit/Controller/Index/RenderTest.php new file mode 100644 index 0000000000000..646cea81212f9 --- /dev/null +++ b/app/code/Magento/Ui/Test/Unit/Controller/Index/RenderTest.php @@ -0,0 +1,403 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Ui\Test\Unit\Controller\Index; + +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Escaper; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Controller\Index\Render; +use Magento\Ui\Model\UiComponentTypeResolver; +use Zend\Http\AbstractMessage; +use Zend\Http\Response; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) + */ +class RenderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Render + */ + private $render; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $responseMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $uiFactoryMock; + + /** + * @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; + + /** + * @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject + */ + private $sessionMock; + + /** + * @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject + */ + private $actionFlagMock; + + /** + * @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $helperMock; + + /** + * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentContextMock; + + /** + * @var \Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface| + * \PHPUnit_Framework_MockObject_MockObject + */ + private $dataProviderMock; + + /** + * @var \Magento\Framework\View\Element\UiComponentInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|UiComponentTypeResolver + */ + private $uiComponentTypeResolverMock; + + /** + * @var \Magento\Framework\Controller\Result\JsonFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultJsonFactoryMock; + + /** + * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + + protected function setUp() + { + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) + ->disableOriginalConstructor() + ->getMock(); + $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock = $this->getMockBuilder(\Magento\Backend\App\Action\Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->uiFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->authorizationMock = $this->getMockBuilder(\Magento\Framework\AuthorizationInterface::class) + ->getMockForAbstractClass(); + $this->sessionMock = $this->getMockBuilder(\Magento\Backend\Model\Session::class) + ->disableOriginalConstructor() + ->getMock(); + $this->actionFlagMock = $this->getMockBuilder(\Magento\Framework\App\ActionFlag::class) + ->disableOriginalConstructor() + ->getMock(); + $this->helperMock = $this->getMockBuilder(\Magento\Backend\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->uiComponentContextMock = $this->getMockForAbstractClass( + ContextInterface::class + ); + $this->dataProviderMock = $this->getMockForAbstractClass( + \Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface::class + ); + $this->uiComponentMock = $this->getMockForAbstractClass( + \Magento\Framework\View\Element\UiComponentInterface::class, + [], + '', + false, + true, + true, + ['render'] + ); + + $this->resultJsonFactoryMock = $this->getMockBuilder( + \Magento\Framework\Controller\Result\JsonFactory::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->loggerMock = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class); + + $this->contextMock->expects($this->any()) + ->method('getRequest') + ->willReturn($this->requestMock); + $this->contextMock->expects($this->any()) + ->method('getResponse') + ->willReturn($this->responseMock); + $this->contextMock->expects($this->any()) + ->method('getAuthorization') + ->willReturn($this->authorizationMock); + $this->contextMock->expects($this->any()) + ->method('getSession') + ->willReturn($this->sessionMock); + $this->contextMock->expects($this->any()) + ->method('getActionFlag') + ->willReturn($this->actionFlagMock); + $this->contextMock->expects($this->any()) + ->method('getHelper') + ->willReturn($this->helperMock); + $this->uiComponentContextMock->expects($this->once()) + ->method('getDataProvider') + ->willReturn($this->dataProviderMock); + $this->uiComponentTypeResolverMock = $this->getMockBuilder(UiComponentTypeResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->escaperMock = $this->createMock(Escaper::class); + $this->escaperMock->expects($this->any()) + ->method('escapeHtml') + ->willReturnArgument(0); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->render = $this->objectManagerHelper->getObject( + \Magento\Ui\Controller\Index\Render::class, + [ + 'context' => $this->contextMock, + 'uiComponentFactory' => $this->uiFactoryMock, + 'contentTypeResolver' => $this->uiComponentTypeResolverMock, + 'resultJsonFactory' => $this->resultJsonFactoryMock, + 'logger' => $this->loggerMock, + 'escaper' => $this->escaperMock, + ] + ); + } + + public function testExecuteException() + { + $name = 'test-name'; + $renderedData = '<html>data</html>'; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->with('namespace') + ->willReturn($name); + $this->requestMock->expects($this->any()) + ->method('getParams') + ->willReturn([]); + $this->responseMock->expects($this->once()) + ->method('appendBody') + ->willThrowException(new \Exception('exception')); + + $jsonResultMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->setMethods(['setData']) + ->getMock(); + + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($jsonResultMock); + + $jsonResultMock->expects($this->once()) + ->method('setData') + ->willReturnSelf(); + + $this->loggerMock->expects($this->once()) + ->method('critical') + ->willReturnSelf(); + + $this->dataProviderMock->expects($this->once()) + ->method('getConfigData') + ->willReturn([]); + + $this->uiComponentMock->expects($this->once()) + ->method('render') + ->willReturn($renderedData); + $this->uiComponentMock->expects($this->once()) + ->method('getChildComponents') + ->willReturn([]); + $this->uiComponentMock->expects($this->once()) + ->method('getContext') + ->willReturn($this->uiComponentContextMock); + $this->uiFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->uiComponentMock); + + $this->render->execute(); + } + + public function testExecute() + { + $name = 'test-name'; + $renderedData = '<html>data</html>'; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->with('namespace') + ->willReturn($name); + $this->requestMock->expects($this->any()) + ->method('getParams') + ->willReturn([]); + $this->responseMock->expects($this->once()) + ->method('appendBody') + ->with($renderedData); + $this->dataProviderMock->expects($this->once()) + ->method('getConfigData') + ->willReturn([]); + + $this->uiComponentMock->expects($this->once()) + ->method('render') + ->willReturn($renderedData); + $this->uiComponentMock->expects($this->once()) + ->method('getChildComponents') + ->willReturn([]); + $this->uiComponentMock->expects($this->any()) + ->method('getContext') + ->willReturn($this->uiComponentContextMock); + $this->uiFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->uiComponentMock); + $this->uiComponentTypeResolverMock->expects($this->once()) + ->method('resolve') + ->with($this->uiComponentContextMock) + ->willReturn('application/json'); + $this->responseMock->expects($this->once())->method('setHeader') + ->with('Content-Type', 'application/json', true); + + $this->render->execute(); + } + + /** + * @param array $dataProviderConfig + * @param bool|null $isAllowed + * @param int $authCallCount + * @dataProvider executeAjaxRequestWithoutPermissionsDataProvider + */ + public function testExecuteWithoutPermissions(array $dataProviderConfig, $isAllowed, $authCallCount = 1) + { + $name = 'test-name'; + $renderedData = '<html>data</html>'; + + if (false === $isAllowed) { + $jsonResultMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->setMethods(['setStatusHeader', 'setData']) + ->getMock(); + + $jsonResultMock->expects($this->at(0)) + ->method('setStatusHeader') + ->with( + Response::STATUS_CODE_403, + AbstractMessage::VERSION_11, + 'Forbidden' + ) + ->willReturnSelf(); + + $jsonResultMock->expects($this->at(1)) + ->method('setData') + ->with( + [ + 'error' => 'Forbidden', + 'errorcode' => 403 + ] + ) + ->willReturnSelf(); + + $this->resultJsonFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($jsonResultMock); + } + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->with('namespace') + ->willReturn($name); + $this->requestMock->expects($this->any()) + ->method('getParams') + ->willReturn([]); + if ($isAllowed === false) { + $this->requestMock->expects($this->once()) + ->method('isAjax') + ->willReturn(true); + } + $this->responseMock->expects($this->never()) + ->method('setRedirect'); + $this->responseMock->expects($this->any()) + ->method('appendBody') + ->with($renderedData); + + $this->dataProviderMock->expects($this->once()) + ->method('getConfigData') + ->willReturn($dataProviderConfig); + + $this->authorizationMock->expects($this->exactly($authCallCount)) + ->method('isAllowed') + ->with(isset($dataProviderConfig['aclResource']) ? $dataProviderConfig['aclResource'] : null) + ->willReturn($isAllowed); + + $this->uiComponentMock->expects($this->any()) + ->method('render') + ->willReturn($renderedData); + $this->uiComponentMock->expects($this->any()) + ->method('getChildComponents') + ->willReturn([]); + $this->uiComponentMock->expects($this->any()) + ->method('getContext') + ->willReturn($this->uiComponentContextMock); + $this->uiFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->uiComponentMock); + + $this->render->execute(); + } + + /** + * @return array + */ + public function executeAjaxRequestWithoutPermissionsDataProvider() + { + $aclResource = 'Magento_Test::index_index'; + return [ + [ + 'dataProviderConfig' => ['aclResource' => $aclResource], + 'isAllowed' => true + ], + [ + 'dataProviderConfig' => ['aclResource' => $aclResource], + 'isAllowed' => false + ], + [ + 'dataProviderConfig' => [], + 'isAllowed' => null, + 'authCallCount' => 0 + ], + ]; + } +} diff --git a/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml b/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml index 9a2703729662a..c4298cd8dc046 100644 --- a/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml +++ b/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** @var $upsModel \Magento\Ups\Helper\Config */ /** @var $block \Magento\Ups\Block\Backend\System\CarrierConfig */ $upsCarrierConfig = $block->getCarrierConfig(); @@ -65,6 +67,7 @@ require(["prototype"], function(){ upsXml.prototype = { initialize: function() { + this.carriersUpsActiveId = 'carriers_ups_active'; this.carriersUpsTypeId = 'carriers_ups_type'; if (!$(this.carriersUpsTypeId)) { return; @@ -92,6 +95,7 @@ require(["prototype"], function(){ this.setFormValues(); Event.observe($(this.carriersUpsTypeId), 'change', this.setFormValues.bind(this)); + Event.observe($(this.carriersUpsActiveId), 'change', this.setFormValues.bind(this)); }, updateAllowedMethods: function(originShipmentTitle) { @@ -146,7 +150,8 @@ require(["prototype"], function(){ $(this.checkingUpsXmlId[a]).removeClassName('required-entry'); } for (a = 0; a < this.checkingUpsId.length; a++) { - $(this.checkingUpsXmlId[a]).addClassName('required-entry'); + $(this.checkingUpsId[a]).addClassName('required-entry'); + this.changeFieldsDisabledState(this.checkingUpsId, a); } Event.stopObserving($('carriers_ups_origin_shipment'), 'change', this.changeOriginShipment.bind(this)); showRowArrayElements(this.onlyUpsElements); @@ -155,9 +160,10 @@ require(["prototype"], function(){ } else { for (a = 0; a < this.checkingUpsXmlId.length; a++) { $(this.checkingUpsXmlId[a]).addClassName('required-entry'); + this.changeFieldsDisabledState(this.checkingUpsXmlId, a); } for (a = 0; a < this.checkingUpsId.length; a++) { - $(this.checkingUpsXmlId[a]).removeClassName('required-entry'); + $(this.checkingUpsId[a]).removeClassName('required-entry'); } Event.observe($('carriers_ups_origin_shipment'), 'change', this.changeOriginShipment.bind(this)); showRowArrayElements(this.onlyUpsXmlElements); @@ -169,6 +175,16 @@ require(["prototype"], function(){ { this.originShipmentTitle = key ? key : $F('carriers_ups_origin_shipment'); this.updateAllowedMethods(this.originShipmentTitle); + }, + changeFieldsDisabledState: function (fields, key) { + $(fields[key]).disabled = $F(this.carriersUpsActiveId) !== '1' + || $(fields[key] + '_inherit') !== null + && $F(fields[key] + '_inherit') === '1'; + + if ($(fields[key]).next() !== undefined) { + $(fields[key]).removeClassName('mage-error').next().remove(); + $(fields[key]).removeAttribute('style'); + } } }; xml = new upsXml(); diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite/UrlResolverIdentity.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite/UrlResolverIdentity.php new file mode 100644 index 0000000000000..1230d96818e13 --- /dev/null +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite/UrlResolverIdentity.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\UrlRewriteGraphQl\Model\Resolver\UrlRewrite; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Get tags from url rewrite entities + */ +class UrlResolverIdentity implements IdentityInterface +{ + /** + * @var IdentityInterface[] + */ + private $urlResolverIdentities = []; + + /** + * @param IdentityInterface[] $urlResolverIdentities + */ + public function __construct( + array $urlResolverIdentities + ) { + $this->urlResolverIdentities = $urlResolverIdentities; + } + + /** + * Get tags for the corespondent url type + * + * @param array $resolvedData + * @return string[] + */ + public function getIdentities(array $resolvedData): array + { + $ids = []; + if (isset($resolvedData['type']) && isset($this->urlResolverIdentities[strtolower($resolvedData['type'])])) { + $ids = $this->urlResolverIdentities[strtolower($resolvedData['type'])] + ->getIdentities($resolvedData); + } + return $ids; + } +} diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index e9033880704ca..92d237d3f01e1 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -2,7 +2,7 @@ # See COPYING.txt for license details. type Query { - urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\EntityUrl") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page") @cache(cacheable: false) + urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\EntityUrl") @doc(description: "The urlResolver query returns the relative URL for a specified product, category or CMS page") @cache(cacheIdentity: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite\\UrlResolverIdentity") } type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `relative_url`, and `type` attributes") { diff --git a/app/code/Magento/User/Block/Buttons.php b/app/code/Magento/User/Block/Buttons.php index f580c5cd72b9b..f39aadd41686e 100644 --- a/app/code/Magento/User/Block/Buttons.php +++ b/app/code/Magento/User/Block/Buttons.php @@ -68,7 +68,7 @@ protected function _prepareLayout() ) . '\', \'' . $this->getUrl( '*/*/delete', ['rid' => $this->getRequest()->getParam('rid')] - ) . '\')', + ) . '\', {data: {}})', 'class' => 'delete' ] ); diff --git a/app/code/Magento/User/Block/Role/Grid/User.php b/app/code/Magento/User/Block/Role/Grid/User.php index 4c6b6378b4c4d..963eed151ccbd 100644 --- a/app/code/Magento/User/Block/Role/Grid/User.php +++ b/app/code/Magento/User/Block/Role/Grid/User.php @@ -85,6 +85,8 @@ protected function _construct() } /** + * Adds column filter to collection + * * @param Column $column * @return $this */ @@ -109,6 +111,8 @@ protected function _addColumnFilterToCollection($column) } /** + * Prepares collection + * * @return $this */ protected function _prepareCollection() @@ -121,6 +125,8 @@ protected function _prepareCollection() } /** + * Prepares columns + * * @return $this */ protected function _prepareColumns() @@ -177,6 +183,8 @@ protected function _prepareColumns() } /** + * Gets grid url + * * @return string */ public function getGridUrl() @@ -186,43 +194,39 @@ public function getGridUrl() } /** + * Gets users + * * @param bool $json * @return string|array */ public function getUsers($json = false) { - if ($this->getRequest()->getParam('in_role_user') != "") { - return $this->getRequest()->getParam('in_role_user'); + $inRoleUser = $this->getRequest()->getParam('in_role_user'); + if ($inRoleUser) { + if ($json) { + return $this->getJSONString($inRoleUser); + } + return $this->escapeJs($this->escapeHtml($inRoleUser)); } - $roleId = $this->getRequest()->getParam( - 'rid' - ) > 0 ? $this->getRequest()->getParam( - 'rid' - ) : $this->_coreRegistry->registry( - 'RID' - ); - + $roleId = $this->getRoleId(); $users = $this->getUsersFormData(); if (false === $users) { $users = $this->_roleFactory->create()->setId($roleId)->getRoleUsers(); } - if (sizeof($users) > 0) { + if (!empty($users)) { if ($json) { $jsonUsers = []; - foreach ($users as $usrid) { - $jsonUsers[$usrid] = 0; + foreach ($users as $userid) { + $jsonUsers[$userid] = 0; } return $this->_jsonEncoder->encode((object)$jsonUsers); - } else { - return array_values($users); - } - } else { - if ($json) { - return '{}'; - } else { - return []; } + return array_values($users); } + if ($json) { + return '{}'; + } + return []; } /** @@ -252,10 +256,37 @@ protected function restoreUsersFormData() \Magento\User\Controller\Adminhtml\User\Role\SaveRole::IN_ROLE_USER_FORM_DATA_SESSION_KEY ); if (null !== $sessionData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction parse_str($sessionData, $sessionData); return array_keys($sessionData); } return false; } + + /** + * Gets role ID + * + * @return string + */ + private function getRoleId() + { + $roleId = $this->getRequest()->getParam('rid'); + if ($roleId <= 0) { + $roleId = $this->_coreRegistry->registry('RID'); + } + return $roleId; + } + + /** + * Gets JSON string + * + * @param string $input + * @return string + */ + private function getJSONString($input) + { + $output = json_decode($input); + return $output ? $this->_jsonEncoder->encode($output) : '{}'; + } } diff --git a/app/code/Magento/User/Block/User/Edit/Tab/Roles.php b/app/code/Magento/User/Block/User/Edit/Tab/Roles.php index 22f89b144587c..96f45c53e96c1 100644 --- a/app/code/Magento/User/Block/User/Edit/Tab/Roles.php +++ b/app/code/Magento/User/Block/User/Edit/Tab/Roles.php @@ -8,6 +8,8 @@ use Magento\Backend\Block\Widget\Grid\Column; /** + * Roles grid + * * @api * @since 100.0.2 */ @@ -68,6 +70,8 @@ protected function _construct() } /** + * Adds column filter to collection + * * @param Column $column * @return $this */ @@ -92,6 +96,8 @@ protected function _addColumnFilterToCollection($column) } /** + * Prepares collection + * * @return $this */ protected function _prepareCollection() @@ -103,6 +109,8 @@ protected function _prepareCollection() } /** + * Prepares columns + * * @return $this */ protected function _prepareColumns() @@ -126,6 +134,8 @@ protected function _prepareColumns() } /** + * Get grid url + * * @return string */ public function getGridUrl() @@ -135,13 +145,20 @@ public function getGridUrl() } /** + * Gets selected roles + * * @param bool $json * @return array|string */ public function getSelectedRoles($json = false) { - if ($this->getRequest()->getParam('user_roles') != "") { - return $this->getRequest()->getParam('user_roles'); + $userRoles = $this->getRequest()->getParam('user_roles'); + if ($userRoles) { + if ($json) { + $result = json_decode($userRoles); + return $result ? $this->_jsonEncoder->encode($result) : '{}'; + } + return $this->escapeJs($this->escapeHtml($userRoles)); } /* @var $user \Magento\User\Model\User */ $user = $this->_coreRegistry->registry('permissions_user'); diff --git a/app/code/Magento/User/Controller/Adminhtml/User/Role/Delete.php b/app/code/Magento/User/Controller/Adminhtml/User/Role/Delete.php index 194e730aedfa7..7997d96945784 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/Role/Delete.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/Role/Delete.php @@ -6,30 +6,41 @@ */ namespace Magento\User\Controller\Adminhtml\User\Role; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; -class Delete extends \Magento\User\Controller\Adminhtml\User\Role +/** + * User roles delete action. + */ +class Delete extends \Magento\User\Controller\Adminhtml\User\Role implements HttpPostActionInterface { /** - * Remove role action + * Remove role action. * - * @return \Magento\Backend\Model\View\Result\Redirect|void + * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - $rid = $this->getRequest()->getParam('rid', false); + $rid = (int)$this->getRequest()->getParam('rid', false); /** @var \Magento\User\Model\User $currentUser */ $currentUser = $this->_userFactory->create()->setId($this->_authSession->getUser()->getId()); if (in_array($rid, $currentUser->getRoles())) { $this->messageManager->addError(__('You cannot delete self-assigned roles.')); + return $resultRedirect->setPath('adminhtml/*/editrole', ['rid' => $rid]); } + $role = $this->_initRole(); + if (!$role->getId()) { + $this->messageManager->addError(__('We can\'t find a role to delete.')); + + return $resultRedirect->setPath("*/*/"); + } try { - $this->_initRole()->delete(); + $role->delete(); $this->messageManager->addSuccess(__('You deleted the role.')); } catch (\Exception $e) { $this->messageManager->addError(__('An error occurred while deleting this role.')); diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index d8040b0bbaaac..dc0aa0cd38343 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -212,14 +212,9 @@ protected function _construct() * Removing dependencies and leaving only entity's properties. * * @return string[] - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $properties = parent::__sleep(); return array_diff( $properties, @@ -245,14 +240,9 @@ public function __sleep() * Restoring required objects after serialization. * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->serializer = $objectManager->get(Json::class); @@ -331,6 +321,7 @@ protected function _getValidationRulesBeforeSave() * Existing user password confirmation will be validated only when password is set * * @return bool|string[] + * @throws \Exception */ public function validate() { @@ -351,6 +342,7 @@ public function validate() * New password is compared to at least 4 previous passwords to prevent setting them again * * @return bool|string[] + * @throws \Exception * @since 100.0.3 */ protected function validatePasswordChange() @@ -403,6 +395,7 @@ public function saveExtra($data) * Retrieve user roles * * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ public function getRoles() { @@ -416,10 +409,6 @@ public function getRoles() */ public function getRole() { - if ($this->getData('extracted_role')) { - $this->_role = $this->getData('extracted_role'); - $this->unsetData('extracted_role'); - } if (null === $this->_role) { $this->_role = $this->_roleFactory->create(); $roles = $this->getRoles(); @@ -455,10 +444,10 @@ public function roleUserExists() /** * Send email with reset password confirmation link. * + * @return $this + * @throws NotificationExceptionInterface * @deprecated * @see NotificatorInterface::sendForgotPassword() - * - * @return $this */ public function sendPasswordResetConfirmationEmail() { @@ -636,9 +625,10 @@ public function verifyIdentity($password) /** * Login user * - * @param string $username - * @param string $password - * @return $this + * @param string $username + * @param string $password + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function login($username, $password) { @@ -725,6 +715,7 @@ public function changeResetPasswordLinkToken($newToken) * Check if current reset password link token is expired * * @return bool + * @throws \Exception */ public function isResetPasswordLinkTokenExpired() { @@ -930,6 +921,7 @@ public function performIdentityCheck($passwordString) { try { $isCheckSuccessful = $this->verifyIdentity($passwordString); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\AuthenticationException $e) { $isCheckSuccessful = false; } diff --git a/app/code/Magento/User/Test/Unit/Block/Role/Grid/UserTest.php b/app/code/Magento/User/Test/Unit/Block/Role/Grid/UserTest.php index 5b03f89cfa553..defdc2b344865 100644 --- a/app/code/Magento/User/Test/Unit/Block/Role/Grid/UserTest.php +++ b/app/code/Magento/User/Test/Unit/Block/Role/Grid/UserTest.php @@ -131,7 +131,6 @@ public function testGetUsersPositiveNumberOfRolesAndJsonFalse() $this->requestInterfaceMock->expects($this->at(0))->method('getParam')->willReturn(""); $this->requestInterfaceMock->expects($this->at(1))->method('getParam')->willReturn($roleId); - $this->requestInterfaceMock->expects($this->at(2))->method('getParam')->willReturn($roleId); $this->registryMock->expects($this->once()) ->method('registry') @@ -158,7 +157,6 @@ public function testGetUsersPositiveNumberOfRolesAndJsonTrue() $this->requestInterfaceMock->expects($this->at(0))->method('getParam')->willReturn(""); $this->requestInterfaceMock->expects($this->at(1))->method('getParam')->willReturn($roleId); - $this->requestInterfaceMock->expects($this->at(2))->method('getParam')->willReturn($roleId); $this->registryMock->expects($this->once()) ->method('registry') @@ -183,7 +181,6 @@ public function testGetUsersNoRolesAndJsonFalse() $this->requestInterfaceMock->expects($this->at(0))->method('getParam')->willReturn(""); $this->requestInterfaceMock->expects($this->at(1))->method('getParam')->willReturn($roleId); - $this->requestInterfaceMock->expects($this->at(2))->method('getParam')->willReturn($roleId); $this->registryMock->expects($this->once()) ->method('registry') @@ -239,4 +236,21 @@ public function testPrepareColumns() $this->model->toHtml(); } + + public function testGetUsersCorrectInRoleUser() + { + $param = 'in_role_user'; + $paramValue = '{"a":"role1","1":"role2","2":"role3"}'; + $this->requestInterfaceMock->expects($this->once())->method('getParam')->with($param)->willReturn($paramValue); + $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturn($paramValue); + $this->assertEquals($paramValue, $this->model->getUsers(true)); + } + + public function testGetUsersIncorrectInRoleUser() + { + $param = 'in_role_user'; + $paramValue = 'not_JSON'; + $this->requestInterfaceMock->expects($this->once())->method('getParam')->with($param)->willReturn($paramValue); + $this->assertEquals('{}', $this->model->getUsers(true)); + } } diff --git a/app/code/Magento/User/Test/Unit/Block/User/Edit/Tab/RolesTest.php b/app/code/Magento/User/Test/Unit/Block/User/Edit/Tab/RolesTest.php new file mode 100644 index 0000000000000..ca7a0a8e17699 --- /dev/null +++ b/app/code/Magento/User/Test/Unit/Block/User/Edit/Tab/RolesTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\Unit\Block\User\Edit\Tab; + +/** + * Class RolesTest to cover \Magento\User\Block\User\Edit\Tab\Roles + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class RolesTest extends \PHPUnit\Framework\TestCase +{ + /** @var \Magento\User\Block\User\Edit\Tab\Roles */ + protected $model; + + /** @var \Magento\Framework\Json\EncoderInterface|\PHPUnit_Framework_MockObject_MockObject */ + protected $jsonEncoderMock; + + /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ + protected $requestInterfaceMock; + + protected function setUp() + { + $this->jsonEncoderMock = $this->getMockBuilder(\Magento\Framework\Json\EncoderInterface::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->requestInterfaceMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->model = $objectManagerHelper->getObject( + \Magento\User\Block\User\Edit\Tab\Roles::class, + [ + 'jsonEncoder' => $this->jsonEncoderMock, + 'request' => $this->requestInterfaceMock, + ] + ); + } + + public function testSelectedRolesCorrectUserRoles() + { + $param = 'user_roles'; + $paramValue = '{"a":"role1","1":"role2","2":"role3"}'; + $this->requestInterfaceMock->expects($this->once())->method('getParam')->with($param)->willReturn($paramValue); + $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturn($paramValue); + $this->assertEquals($paramValue, $this->model->getSelectedRoles(true)); + } + + public function testSelectedRolesIncorrectUserRoles() + { + $param = 'user_roles'; + $paramValue = 'not_JSON'; + $this->requestInterfaceMock->expects($this->once())->method('getParam')->with($param)->willReturn($paramValue); + $this->assertEquals('{}', $this->model->getSelectedRoles(true)); + } +} diff --git a/app/code/Magento/User/Test/Unit/Controller/Adminhtml/User/Role/DeleteTest.php b/app/code/Magento/User/Test/Unit/Controller/Adminhtml/User/Role/DeleteTest.php new file mode 100644 index 0000000000000..7780c43ecaf16 --- /dev/null +++ b/app/code/Magento/User/Test/Unit/Controller/Adminhtml/User/Role/DeleteTest.php @@ -0,0 +1,314 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\User\Test\Unit\Controller\Adminhtml\User\Role; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * Unit tests for \Magento\User\Controller\Adminhtml\User\Role\Delete. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DeleteTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\User\Controller\Adminhtml\User\Role\Delete + */ + private $controller; + + /** + * @var \Magento\Framework\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Authorization\Model\RoleFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $roleFactoryMock; + + /** + * @var \Magento\User\Model\UserFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $userFactoryMock; + + /** + * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject + */ + private $coreRegistryMock; + + /** + * @var \Magento\Authorization\Model\RulesFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $rulesFactoryMock; + + /** + * @var \Magento\Backend\Model\Auth\Session|\PHPUnit_Framework_MockObject_MockObject + */ + private $authSessionMock; + + /** + * @var \Magento\Framework\Filter\FilterManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $filterManagerMock; + + /** + * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectMock; + + /** + * @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultFactoryMock; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject + */ + private $messageManagerMock; + + /** + * @var \Magento\Authorization\Model\Role|\PHPUnit_Framework_MockObject_MockObject + */ + private $roleModelMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManagerHelper = new ObjectManagerHelper($this); + + $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); + $this->coreRegistryMock = $this->createPartialMock(\Magento\Framework\Registry::class, ['getId']); + $this->roleFactoryMock = $this->createMock(\Magento\Authorization\Model\RoleFactory::class); + $this->userFactoryMock = $this->createPartialMock( + \Magento\User\Model\UserFactory::class, + ['create'] + ); + $this->rulesFactoryMock = $this->createMock(\Magento\Authorization\Model\RulesFactory::class); + $this->authSessionMock = $this->createPartialMock( + \Magento\Backend\Model\Auth\Session::class, + ['getUser'] + ); + $this->filterManagerMock = $this->createMock(\Magento\Framework\Filter\FilterManager::class); + $this->resultRedirectMock = $this->createPartialMock( + \Magento\Backend\Model\View\Result\Redirect::class, + ['setPath'] + ); + $this->resultFactoryMock = $this->createPartialMock( + \Magento\Framework\Controller\ResultFactory::class, + ['create'] + ); + + $this->resultFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($this->resultRedirectMock); + $this->requestMock = $this->getMockForAbstractClass( + \Magento\Framework\App\RequestInterface::class, + [], + '', + false, + true, + true, + ['getParam'] + ); + $this->contextMock->expects($this->once()) + ->method('getResultFactory') + ->willReturn($this->resultFactoryMock); + $this->resultFactoryMock->expects($this->once()) + ->method('create') + ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) + ->willReturn($this->resultRedirectMock); + + $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); + $this->contextMock->expects($this->once()) + ->method('getMessageManager') + ->willReturn($this->messageManagerMock); + + $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); + + $this->roleModelMock = $this->createPartialMock( + \Magento\Authorization\Model\Role::class, + ['load', 'getId', 'getRoleType', 'delete'] + ); + + $this->controller = $objectManagerHelper->getObject( + \Magento\User\Controller\Adminhtml\User\Role\Delete::class, + [ + 'context' => $this->contextMock, + 'coreRegistry' => $this->coreRegistryMock, + 'roleFactory' => $this->roleFactoryMock, + 'userFactory' => $this->userFactoryMock, + 'rulesFactory' => $this->rulesFactoryMock, + 'authSession' => $this->authSessionMock, + 'filterManager' => $this->filterManagerMock, + ] + ); + } + + /** + * Unit test which trying to delete role which assigned on current user. + * + * @return void + */ + public function testExecuteDeleteSelfAssignedRole() + { + $idUser = 1; + $idUserRole = 3; + $idDeleteRole = 3; + + $this->checkUserAndRoleIds($idDeleteRole, $idUser, $idUserRole); + + $this->messageManagerMock->expects($this->once()) + ->method('addError') + ->with(__('You cannot delete self-assigned roles.')) + ->willReturnSelf(); + + $this->resultRedirectMock->expects($this->once()) + ->method('setPath') + ->with('adminhtml/*/editrole', ['rid' => $idDeleteRole]) + ->willReturnSelf(); + + $this->controller->execute(); + } + + /** + * Unit test which trying to delete role. + * + * @return void + */ + public function testExecuteDeleteWithNormalScenario() + { + $idUser = 1; + $idUserRole = 3; + $idDeleteRole = 5; + $roleType = 'G'; + + $this->checkUserAndRoleIds($idDeleteRole, $idUser, $idUserRole); + + $this->initRoleExecute($roleType); + $this->roleModelMock->expects($this->exactly(2))->method('getId')->willReturn($idDeleteRole); + + $this->roleModelMock->expects($this->once())->method('delete')->willReturnSelf(); + + $this->messageManagerMock->expects($this->once()) + ->method('addSuccess') + ->with(__('You deleted the role.')) + ->willReturnSelf(); + + $this->resultRedirectMock->expects($this->once()) + ->method('setPath') + ->with('*/*/') + ->willReturnSelf(); + + $this->controller->execute(); + } + + /** + * Unit test which failed on delete role. + * + * @return void + */ + public function testExecuteDeleteWithError() + { + $idUser = 1; + $idUserRole = 3; + $idDeleteRole = 5; + $roleType = 'G'; + + $this->checkUserAndRoleIds($idDeleteRole, $idUser, $idUserRole); + + $this->initRoleExecute($roleType); + $this->roleModelMock->expects($this->exactly(2))->method('getId')->willReturn($idDeleteRole); + + $this->roleModelMock->expects($this->once())->method('delete')->willThrowException(new \Exception); + + $this->messageManagerMock->expects($this->once()) + ->method('addError') + ->with(__('An error occurred while deleting this role.')) + ->willReturnSelf(); + + $this->resultRedirectMock->expects($this->once()) + ->method('setPath') + ->with('*/*/') + ->willReturnSelf(); + + $this->controller->execute(); + } + + /** + * Unit test which trying to delete nonexistent role. + * + * @return void + */ + public function testExecuteWithoutRole() + { + $idUser = 1; + $idUserRole = 3; + $idDeleteRole = 100; + $roleType = null; + + $this->checkUserAndRoleIds($idDeleteRole, $idUser, $idUserRole); + + $this->initRoleExecute($roleType); + $this->roleModelMock->expects($this->at(1))->method('getId')->willReturn($idDeleteRole); + $this->roleModelMock->expects($this->at(2))->method('getId')->willReturn(null); + + $this->messageManagerMock->expects($this->once()) + ->method('addError') + ->with(__('We can\'t find a role to delete.')) + ->willReturnSelf(); + + $this->resultRedirectMock->expects($this->once()) + ->method('setPath') + ->with('*/*/') + ->willReturnSelf(); + + $this->controller->execute(); + } + + /** + * Method which takes Id from request and check with User Role Id. + * + * @param int $id + * @param int $userId + * @param int $userRoleId + * @return void + */ + private function checkUserAndRoleIds(int $id, int $userId, int $userRoleId) + { + $this->requestMock->expects($this->atLeastOnce())->method('getParam')->with('rid')->willReturn($id); + + $userModelMock = $this->createPartialMock(\Magento\User\Model\User::class, ['getId', 'setId', 'getRoles']); + $this->authSessionMock->expects($this->once())->method('getUser')->willReturn($userModelMock); + $userModelMock->expects($this->once())->method('getId')->willReturn($userId); + + $this->userFactoryMock->expects($this->once())->method('create')->willReturn($userModelMock); + $userModelMock->expects($this->once())->method('setId')->with($userId)->willReturnSelf(); + + $userModelMock->expects($this->once())->method('getRoles')->willReturn(['id' => $userRoleId]); + } + + /** + * Execute initialization Role. + * + * @param string|null $roleType + * @return void + */ + private function initRoleExecute($roleType) + { + $this->roleFactoryMock->expects($this->once())->method('create')->willReturn($this->roleModelMock); + $this->roleModelMock->expects($this->once())->method('load')->willReturnSelf(); + $this->roleModelMock->expects($this->once())->method('getRoleType')->willReturn($roleType); + } +} diff --git a/app/code/Magento/User/Test/Unit/Model/Authorization/AdminSessionUserContextTest.php b/app/code/Magento/User/Test/Unit/Model/Authorization/AdminSessionUserContextTest.php new file mode 100644 index 0000000000000..23681c4b8da26 --- /dev/null +++ b/app/code/Magento/User/Test/Unit/Model/Authorization/AdminSessionUserContextTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\Unit\Model\Authorization; + +use Magento\Authorization\Model\UserContextInterface; + +/** + * Tests Magento\User\Model\Authorization\AdminSessionUserContext + */ +class AdminSessionUserContextTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + protected $objectManager; + + /** + * @var \Magento\User\Model\Authorization\AdminSessionUserContext + */ + protected $adminSessionUserContext; + + /** + * @var \Magento\Backend\Model\Auth\Session + */ + protected $adminSession; + + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->adminSession = $this->getMockBuilder(\Magento\Backend\Model\Auth\Session::class) + ->disableOriginalConstructor() + ->setMethods(['hasUser', 'getUser', 'getId']) + ->getMock(); + + $this->adminSessionUserContext = $this->objectManager->getObject( + \Magento\User\Model\Authorization\AdminSessionUserContext::class, + ['adminSession' => $this->adminSession] + ); + } + + public function testGetUserIdExist() + { + $userId = 1; + + $this->setupUserId($userId); + + $this->assertEquals($userId, $this->adminSessionUserContext->getUserId()); + } + + public function testGetUserIdDoesNotExist() + { + $userId = null; + + $this->setupUserId($userId); + + $this->assertEquals($userId, $this->adminSessionUserContext->getUserId()); + } + + public function testGetUserType() + { + $this->assertEquals(UserContextInterface::USER_TYPE_ADMIN, $this->adminSessionUserContext->getUserType()); + } + + /** + * @param int|null $userId + * @return void + */ + public function setupUserId($userId) + { + $this->adminSession->expects($this->once()) + ->method('hasUser') + ->will($this->returnValue($userId)); + + if ($userId) { + $this->adminSession->expects($this->once()) + ->method('getUser') + ->will($this->returnSelf()); + + $this->adminSession->expects($this->once()) + ->method('getId') + ->will($this->returnValue($userId)); + } + } +} diff --git a/app/code/Magento/User/Test/Unit/Model/UserTest.php b/app/code/Magento/User/Test/Unit/Model/UserTest.php index ab06c8754b2f0..670316c2500fc 100644 --- a/app/code/Magento/User/Test/Unit/Model/UserTest.php +++ b/app/code/Magento/User/Test/Unit/Model/UserTest.php @@ -44,6 +44,31 @@ protected function setUp() ); } + /** + * @return void + */ + public function testSleep() + { + $excludedProperties = [ + '_eventManager', + '_cacheManager', + '_registry', + '_appState', + '_userData', + '_config', + '_validatorObject', + '_roleFactory', + '_encryptor', + '_transportBuilder', + '_storeManager', + '_validatorBeforeSave' + ]; + $actualResult = $this->model->__sleep(); + $this->assertNotEmpty($actualResult); + $expectedResult = array_intersect($actualResult, $excludedProperties); + $this->assertEmpty($expectedResult); + } + /** * @return void */ diff --git a/app/code/Magento/User/view/adminhtml/templates/role/users_grid_js.phtml b/app/code/Magento/User/view/adminhtml/templates/role/users_grid_js.phtml index 5505d34322fda..a3b5dc68050ac 100644 --- a/app/code/Magento/User/view/adminhtml/templates/role/users_grid_js.phtml +++ b/app/code/Magento/User/view/adminhtml/templates/role/users_grid_js.phtml @@ -12,7 +12,6 @@ require([ 'mage/adminhtml/grid', 'prototype' ], function(jQuery, confirm, _){ -<!-- <?php $myBlock = $block->getLayout()->getBlock('roleUsersGrid'); ?> <?php if (is_object($myBlock) && $myBlock->getJsObjectName()) : ?> var checkBoxes = $H(<?= /* @noEscape */ $myBlock->getUsers(true) ?>); @@ -133,7 +132,6 @@ require([ } onLoad(); <?php endif; ?> -//--> }); </script> diff --git a/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml b/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml new file mode 100644 index 0000000000000..c9d4cb3391cfd --- /dev/null +++ b/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectVault"> + <annotations> + <features value="Vault"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Vault Pages"/> + <description value="Verify that the Secure URL configuration applies to the Vault pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15562"/> + <group value="vault"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/vault" stepKey="goToUnsecureVaultURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/vault" stepKey="seeSecureVaultURL"/> + </test> +</tests> diff --git a/app/code/Magento/Vault/Test/Unit/Observer/AfterPaymentSaveObserverTest.php b/app/code/Magento/Vault/Test/Unit/Observer/AfterPaymentSaveObserverTest.php index 2ae16e186030d..501ab22a80385 100644 --- a/app/code/Magento/Vault/Test/Unit/Observer/AfterPaymentSaveObserverTest.php +++ b/app/code/Magento/Vault/Test/Unit/Observer/AfterPaymentSaveObserverTest.php @@ -21,7 +21,7 @@ use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * Tests for AfterPaymentSaveObserver. + * Test for payment observer. */ class AfterPaymentSaveObserverTest extends \PHPUnit\Framework\TestCase { @@ -66,7 +66,7 @@ class AfterPaymentSaveObserverTest extends \PHPUnit\Framework\TestCase protected $salesOrderPaymentMock; /** - * @return void + * @inheritdoc */ protected function setUp() { @@ -74,6 +74,10 @@ protected function setUp() $encryptorRandomGenerator = $this->createMock(Random::class); /** @var DeploymentConfig|MockObject $deploymentConfigMock */ $deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $deploymentConfigMock->expects($this->any()) + ->method('get') + ->with(Encryptor::PARAM_CRYPT_KEY) + ->willReturn('g9mY9KLrcuAVJfsmVUSRkKFLDdUPVkaZ'); $this->encryptorModel = new Encryptor($encryptorRandomGenerator, $deploymentConfigMock); $this->paymentExtension = $this->getMockBuilder(OrderPaymentExtension::class) @@ -122,6 +126,8 @@ protected function setUp() } /** + * Case when payment successfully made. + * * @param int $customerId * @param string $createdAt * @param string $token @@ -173,6 +179,8 @@ public function testPositiveCase($customerId, $createdAt, $token, $isActive, $me } /** + * Data for positiveCase test. + * * @return array */ public function positiveCaseDataProvider() diff --git a/app/code/Magento/Vault/etc/frontend/di.xml b/app/code/Magento/Vault/etc/frontend/di.xml index cb7ab4735d470..b25ac65aae5f1 100644 --- a/app/code/Magento/Vault/etc/frontend/di.xml +++ b/app/code/Magento/Vault/etc/frontend/di.xml @@ -23,4 +23,11 @@ <plugin name="ProcessPaymentVaultConfiguration" type="Magento\Vault\Plugin\PaymentVaultConfigurationProcess"/> <plugin name="ProcessPaymentConfiguration" disabled="true"/> </type> + <type name="Magento\Framework\Url\SecurityInfo"> + <arguments> + <argument name="secureUrlList" xsi:type="array"> + <item name="vault" xsi:type="string">/vault/</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Webapi/Controller/Rest/ParamsOverrider.php b/app/code/Magento/Webapi/Controller/Rest/ParamsOverrider.php index d1a3c0c0f0864..be11ee5a11c76 100644 --- a/app/code/Magento/Webapi/Controller/Rest/ParamsOverrider.php +++ b/app/code/Magento/Webapi/Controller/Rest/ParamsOverrider.php @@ -6,6 +6,7 @@ namespace Magento\Webapi\Controller\Rest; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Webapi\Rest\Request\ParamOverriderInterface; use Magento\Webapi\Model\Config\Converter; use Magento\Framework\Reflection\MethodsMap; @@ -26,15 +27,24 @@ class ParamsOverrider */ private $methodsMap; + /** + * @var SimpleDataObjectConverter + */ + private $dataObjectConverter; + /** * Initialize dependencies * * @param ParamOverriderInterface[] $paramOverriders + * @param SimpleDataObjectConverter|null $dataObjectConverter */ public function __construct( - array $paramOverriders = [] + array $paramOverriders = [], + SimpleDataObjectConverter $dataObjectConverter = null ) { $this->paramOverriders = $paramOverriders; + $this->dataObjectConverter = $dataObjectConverter + ?? ObjectManager::getInstance()->get(SimpleDataObjectConverter::class); } /** @@ -64,15 +74,17 @@ public function override(array $inputData, array $parameters) /** * Determine if a nested array value is set. * - * @param array &$nestedArray + * @param array $nestedArray * @param string[] $arrayKeys * @return bool true if array value is set */ - protected function isNestedArrayValueSet(&$nestedArray, $arrayKeys) + protected function isNestedArrayValueSet($nestedArray, $arrayKeys) { - $currentArray = &$nestedArray; + //Converting input data to camelCase in order to process both snake and camel style data equally. + $currentArray = $this->dataObjectConverter->convertKeysToCamelCase($nestedArray); foreach ($arrayKeys as $key) { + $key = SimpleDataObjectConverter::snakeCaseToCamelCase($key); if (!isset($currentArray[$key])) { return false; } @@ -95,12 +107,22 @@ protected function setNestedArrayValue(&$nestedArray, $arrayKeys, $valueToSet) $lastKey = array_pop($arrayKeys); foreach ($arrayKeys as $key) { + if (!array_key_exists($key, $currentArray)) { + //In case input data uses camelCase format + $key = SimpleDataObjectConverter::snakeCaseToCamelCase($key); + } if (!isset($currentArray[$key])) { $currentArray[$key] = []; } $currentArray = &$currentArray[$key]; } + //In case input data uses camelCase format + $camelCaseKey = SimpleDataObjectConverter::snakeCaseToCamelCase($lastKey); + if (array_key_exists($camelCaseKey, $currentArray)) { + $lastKey = $camelCaseKey; + } + $currentArray[$lastKey] = $valueToSet; } diff --git a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php index 486275ac69dc5..5e50cdee794ce 100644 --- a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php +++ b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php @@ -9,12 +9,14 @@ use Magento\Framework\Api\ExtensibleDataInterface; use Magento\Framework\Api\MetadataObjectInterface; use Magento\Framework\Api\SimpleDataObjectConverter; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Webapi\Authorization; use Magento\Framework\Exception\AuthorizationException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Webapi\ServiceInputProcessor; use Magento\Framework\Webapi\Request as SoapRequest; use Magento\Framework\Webapi\Exception as WebapiException; +use Magento\Webapi\Controller\Rest\ParamsOverrider; use Magento\Webapi\Model\Soap\Config as SoapConfig; use Magento\Framework\Reflection\MethodsMap; use Magento\Webapi\Model\ServiceMetadata; @@ -70,6 +72,11 @@ class Handler */ protected $methodsMapProcessor; + /** + * @var ParamsOverrider + */ + private $paramsOverrider; + /** * Initialize dependencies. * @@ -81,6 +88,7 @@ class Handler * @param ServiceInputProcessor $serviceInputProcessor * @param DataObjectProcessor $dataObjectProcessor * @param MethodsMap $methodsMapProcessor + * @param ParamsOverrider|null $paramsOverrider */ public function __construct( SoapRequest $request, @@ -90,7 +98,8 @@ public function __construct( SimpleDataObjectConverter $dataObjectConverter, ServiceInputProcessor $serviceInputProcessor, DataObjectProcessor $dataObjectProcessor, - MethodsMap $methodsMapProcessor + MethodsMap $methodsMapProcessor, + ?ParamsOverrider $paramsOverrider = null ) { $this->_request = $request; $this->_objectManager = $objectManager; @@ -100,6 +109,7 @@ public function __construct( $this->serviceInputProcessor = $serviceInputProcessor; $this->_dataObjectProcessor = $dataObjectProcessor; $this->methodsMapProcessor = $methodsMapProcessor; + $this->paramsOverrider = $paramsOverrider ?? ObjectManager::getInstance()->get(ParamsOverrider::class); } /** @@ -133,25 +143,52 @@ public function __call($operation, $arguments) ); } $service = $this->_objectManager->get($serviceClass); - $inputData = $this->_prepareRequestData($serviceClass, $serviceMethod, $arguments); + $inputData = $this->prepareOperationInput($serviceClass, $serviceMethodInfo, $arguments); $outputData = call_user_func_array([$service, $serviceMethod], $inputData); return $this->_prepareResponseData($outputData, $serviceClass, $serviceMethod); } /** - * Convert SOAP operation arguments into format acceptable by service method. + * Convert arguments received from SOAP server to arguments to pass to a service. * * @param string $serviceClass - * @param string $serviceMethod + * @param array $methodMetadata * @param array $arguments * @return array + * @throws WebapiException + * @throws \Magento\Framework\Exception\InputException */ - protected function _prepareRequestData($serviceClass, $serviceMethod, $arguments) + private function prepareOperationInput(string $serviceClass, array $methodMetadata, array $arguments): array { /** SoapServer wraps parameters into array. Thus this wrapping should be removed to get access to parameters. */ $arguments = reset($arguments); $arguments = $this->_dataObjectConverter->convertStdObjectToArray($arguments, true); - return $this->serviceInputProcessor->process($serviceClass, $serviceMethod, $arguments); + $arguments = $this->paramsOverrider->override($arguments, $methodMetadata[ServiceMetadata::KEY_ROUTE_PARAMS]); + + return $this->serviceInputProcessor->process( + $serviceClass, + $methodMetadata[ServiceMetadata::KEY_METHOD], + $arguments + ); + } + + /** + * Convert SOAP operation arguments into format acceptable by service method. + * + * @param string $serviceClass + * @param string $serviceMethod + * @param array $arguments + * @return array + * @deprecated + * @see Handler::prepareOperationInput() + */ + protected function _prepareRequestData($serviceClass, $serviceMethod, $arguments) + { + return $this->prepareOperationInput( + $serviceClass, + [ServiceMetadata::KEY_METHOD => $serviceMethod, ServiceMetadata::KEY_ROUTE_PARAMS => []], + $arguments + ); } /** diff --git a/app/code/Magento/Webapi/Model/Config/ClassReflector.php b/app/code/Magento/Webapi/Model/Config/ClassReflector.php index 6748319f9f482..b73e4e0afb585 100644 --- a/app/code/Magento/Webapi/Model/Config/ClassReflector.php +++ b/app/code/Magento/Webapi/Model/Config/ClassReflector.php @@ -31,7 +31,7 @@ public function __construct(\Magento\Framework\Reflection\TypeProcessor $typePro * Reflect methods in given class and set retrieved data into reader. * * @param string $className - * @param array $methods + * @param string[]|array $methods List of methods of methods' metadata. * @return array <pre>array( * $firstMethod => array( * 'documentation' => $methodDocumentation, @@ -68,7 +68,7 @@ public function reflectClassMethods($className, $methods) /** @var \Zend\Code\Reflection\MethodReflection $methodReflection */ foreach ($classReflection->getMethods() as $methodReflection) { $methodName = $methodReflection->getName(); - if (array_key_exists($methodName, $methods)) { + if (in_array($methodName, $methods) || array_key_exists($methodName, $methods)) { $data[$methodName] = $this->extractMethodData($methodReflection); } } diff --git a/app/code/Magento/Webapi/Model/Config/Converter.php b/app/code/Magento/Webapi/Model/Config/Converter.php index a85fcbb15329f..837a0f84423ad 100644 --- a/app/code/Magento/Webapi/Model/Config/Converter.php +++ b/app/code/Magento/Webapi/Model/Config/Converter.php @@ -28,10 +28,11 @@ class Converter implements \Magento\Framework\Config\ConverterInterface const KEY_METHOD = 'method'; const KEY_METHODS = 'methods'; const KEY_DESCRIPTION = 'description'; + const KEY_REAL_SERVICE_METHOD = 'realMethod'; /**#@-*/ /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -49,6 +50,10 @@ public function convert($source) $service = $route->getElementsByTagName('service')->item(0); $serviceClass = $service->attributes->getNamedItem('class')->nodeValue; $serviceMethod = $service->attributes->getNamedItem('method')->nodeValue; + $soapMethod = $serviceMethod; + if ($soapOperationNode = $route->attributes->getNamedItem('soapOperation')) { + $soapMethod = trim($soapOperationNode->nodeValue); + } $url = trim($route->attributes->getNamedItem('url')->nodeValue); $version = $this->convertVersion($url); @@ -70,23 +75,25 @@ public function convert($source) // For SOAP $resourcePermissionSet[] = $ref; } + $data = $this->convertMethodParameters($route->getElementsByTagName('parameter')); + $serviceData = $data; - if (!isset($serviceClassData[self::KEY_METHODS][$serviceMethod])) { - $serviceClassData[self::KEY_METHODS][$serviceMethod][self::KEY_ACL_RESOURCES] = $resourcePermissionSet; + if (!isset($serviceClassData[self::KEY_METHODS][$soapMethod])) { + $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_ACL_RESOURCES] = $resourcePermissionSet; } else { - $serviceClassData[self::KEY_METHODS][$serviceMethod][self::KEY_ACL_RESOURCES] = + $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_ACL_RESOURCES] = array_unique( array_merge( - $serviceClassData[self::KEY_METHODS][$serviceMethod][self::KEY_ACL_RESOURCES], + $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_ACL_RESOURCES], $resourcePermissionSet ) ); + $serviceData = []; } $method = $route->attributes->getNamedItem('method')->nodeValue; $secureNode = $route->attributes->getNamedItem('secure'); $secure = $secureNode ? (bool)trim($secureNode->nodeValue) : false; - $data = $this->convertMethodParameters($route->getElementsByTagName('parameter')); // We could handle merging here by checking if the route already exists $result[self::KEY_ROUTES][$url][$method] = [ @@ -100,10 +107,14 @@ public function convert($source) ]; $serviceSecure = false; - if (isset($serviceClassData[self::KEY_METHODS][$serviceMethod][self::KEY_SECURE])) { - $serviceSecure = $serviceClassData[self::KEY_METHODS][$serviceMethod][self::KEY_SECURE]; + if (isset($serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_SECURE])) { + $serviceSecure = $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_SECURE]; } - $serviceClassData[self::KEY_METHODS][$serviceMethod][self::KEY_SECURE] = $serviceSecure || $secure; + if (!isset($serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_REAL_SERVICE_METHOD])) { + $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_REAL_SERVICE_METHOD] = $serviceMethod; + } + $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_SECURE] = $serviceSecure || $secure; + $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_DATA_PARAMETERS] = $serviceData; $result[self::KEY_SERVICES][$serviceClass][$version] = $serviceClassData; } @@ -147,7 +158,8 @@ protected function convertMethodParameters($parameters) /** * Derive the version from the provided URL. - * Assumes the version is the first portion of the URL. For example, '/V1/customers' + * + * Assumes the version is the first portion of the URL. For example, '/V1/customers'. * * @param string $url * @return string diff --git a/app/code/Magento/Webapi/Model/Config/Reader.php b/app/code/Magento/Webapi/Model/Config/Reader.php index 15eb1c0fb8e39..fbdef3c5dc62c 100644 --- a/app/code/Magento/Webapi/Model/Config/Reader.php +++ b/app/code/Magento/Webapi/Model/Config/Reader.php @@ -18,38 +18,6 @@ class Reader extends \Magento\Framework\Config\Reader\Filesystem protected $_idAttributes = [ '/routes/route' => ['url', 'method'], '/routes/route/resources/resource' => 'ref', - '/routes/route/data' => 'name', + '/routes/route/data/parameter' => 'name', ]; - - /** - * @param \Magento\Framework\Config\FileResolverInterface $fileResolver - * @param Converter $converter - * @param SchemaLocator $schemaLocator - * @param \Magento\Framework\Config\ValidationStateInterface $validationState - * @param string $fileName - * @param array $idAttributes - * @param string $domDocumentClass - * @param string $defaultScope - */ - public function __construct( - \Magento\Framework\Config\FileResolverInterface $fileResolver, - Converter $converter, - SchemaLocator $schemaLocator, - \Magento\Framework\Config\ValidationStateInterface $validationState, - $fileName = 'webapi.xml', - $idAttributes = [], - $domDocumentClass = \Magento\Framework\Config\Dom::class, - $defaultScope = 'global' - ) { - parent::__construct( - $fileResolver, - $converter, - $schemaLocator, - $validationState, - $fileName, - $idAttributes, - $domDocumentClass, - $defaultScope - ); - } } diff --git a/app/code/Magento/Webapi/Model/Config/SchemaLocator.php b/app/code/Magento/Webapi/Model/Config/SchemaLocator.php index 21111f0e9df38..b94f96c8ba294 100644 --- a/app/code/Magento/Webapi/Model/Config/SchemaLocator.php +++ b/app/code/Magento/Webapi/Model/Config/SchemaLocator.php @@ -31,7 +31,8 @@ class SchemaLocator implements \Magento\Framework\Config\SchemaLocatorInterface */ public function __construct(\Magento\Framework\Module\Dir\Reader $moduleReader) { - $this->_schema = $moduleReader->getModuleDir(Dir::MODULE_ETC_DIR, 'Magento_Webapi') . '/webapi.xsd'; + $this->_schema = $moduleReader->getModuleDir(Dir::MODULE_ETC_DIR, 'Magento_Webapi') . '/webapi_merged.xsd'; + $this->_perFileSchema = $moduleReader->getModuleDir(Dir::MODULE_ETC_DIR, 'Magento_Webapi') . '/webapi.xsd'; } /** diff --git a/app/code/Magento/Webapi/Model/ServiceMetadata.php b/app/code/Magento/Webapi/Model/ServiceMetadata.php index b56aa84b94651..36f5819b03c98 100644 --- a/app/code/Magento/Webapi/Model/ServiceMetadata.php +++ b/app/code/Magento/Webapi/Model/ServiceMetadata.php @@ -36,6 +36,8 @@ class ServiceMetadata const KEY_ROUTE_PARAMS = 'parameters'; + const KEY_METHOD_ALIAS = 'methodAlias'; + const SERVICES_CONFIG_CACHE_ID = 'services-services-config'; const ROUTES_CONFIG_CACHE_ID = 'routes-services-config'; @@ -113,23 +115,31 @@ protected function initServicesMetadata() foreach ($this->config->getServices()[Converter::KEY_SERVICES] as $serviceClass => $serviceVersionData) { foreach ($serviceVersionData as $version => $serviceData) { $serviceName = $this->getServiceName($serviceClass, $version); + $methods = []; foreach ($serviceData[Converter::KEY_METHODS] as $methodName => $methodMetadata) { $services[$serviceName][self::KEY_SERVICE_METHODS][$methodName] = [ - self::KEY_METHOD => $methodName, + self::KEY_METHOD => $methodMetadata[Converter::KEY_REAL_SERVICE_METHOD], self::KEY_IS_REQUIRED => (bool)$methodMetadata[Converter::KEY_SECURE], self::KEY_IS_SECURE => $methodMetadata[Converter::KEY_SECURE], self::KEY_ACL_RESOURCES => $methodMetadata[Converter::KEY_ACL_RESOURCES], + self::KEY_METHOD_ALIAS => $methodName, + self::KEY_ROUTE_PARAMS => $methodMetadata[Converter::KEY_DATA_PARAMETERS] ]; $services[$serviceName][self::KEY_CLASS] = $serviceClass; + $methods[] = $methodMetadata[Converter::KEY_REAL_SERVICE_METHOD]; } + unset($methodName, $methodMetadata); $reflectedMethodsMetadata = $this->classReflector->reflectClassMethods( $serviceClass, - $services[$serviceName][self::KEY_SERVICE_METHODS] - ); - $services[$serviceName][self::KEY_SERVICE_METHODS] = array_merge_recursive( - $services[$serviceName][self::KEY_SERVICE_METHODS], - $reflectedMethodsMetadata + $methods ); + foreach ($services[$serviceName][self::KEY_SERVICE_METHODS] as $methodName => &$methodMetadata) { + $methodMetadata = array_merge( + $methodMetadata, + $reflectedMethodsMetadata[$methodMetadata[self::KEY_METHOD]] + ); + } + unset($methodName, $methodMetadata); $services[$serviceName][Converter::KEY_DESCRIPTION] = $this->classReflector->extractClassDescription( $serviceClass ); diff --git a/app/code/Magento/Webapi/Model/Soap/Config.php b/app/code/Magento/Webapi/Model/Soap/Config.php index ea269d8703a47..190280ff8f004 100644 --- a/app/code/Magento/Webapi/Model/Soap/Config.php +++ b/app/code/Magento/Webapi/Model/Soap/Config.php @@ -75,12 +75,14 @@ protected function getSoapOperations($requestedServices) foreach ($serviceData[ServiceMetadata::KEY_SERVICE_METHODS] as $methodData) { $method = $methodData[ServiceMetadata::KEY_METHOD]; $class = $serviceData[ServiceMetadata::KEY_CLASS]; - $operationName = $serviceName . ucfirst($method); + $operation = $methodData[ServiceMetadata::KEY_METHOD_ALIAS]; + $operationName = $serviceName . ucfirst($operation); $this->soapOperations[$operationName] = [ ServiceMetadata::KEY_CLASS => $class, ServiceMetadata::KEY_METHOD => $method, ServiceMetadata::KEY_IS_SECURE => $methodData[ServiceMetadata::KEY_IS_SECURE], ServiceMetadata::KEY_ACL_RESOURCES => $methodData[ServiceMetadata::KEY_ACL_RESOURCES], + ServiceMetadata::KEY_ROUTE_PARAMS => $methodData[ServiceMetadata::KEY_ROUTE_PARAMS] ]; } } @@ -110,7 +112,8 @@ public function getServiceMethodInfo($soapOperation, $requestedServices) ServiceMetadata::KEY_CLASS => $soapOperations[$soapOperation][ServiceMetadata::KEY_CLASS], ServiceMetadata::KEY_METHOD => $soapOperations[$soapOperation][ServiceMetadata::KEY_METHOD], ServiceMetadata::KEY_IS_SECURE => $soapOperations[$soapOperation][ServiceMetadata::KEY_IS_SECURE], - ServiceMetadata::KEY_ACL_RESOURCES => $soapOperations[$soapOperation][ServiceMetadata::KEY_ACL_RESOURCES] + ServiceMetadata::KEY_ACL_RESOURCES => $soapOperations[$soapOperation][ServiceMetadata::KEY_ACL_RESOURCES], + ServiceMetadata::KEY_ROUTE_PARAMS => $soapOperations[$soapOperation][ServiceMetadata::KEY_ROUTE_PARAMS] ]; } diff --git a/app/code/Magento/Webapi/Test/Unit/Controller/Rest/ParamsOverriderTest.php b/app/code/Magento/Webapi/Test/Unit/Controller/Rest/ParamsOverriderTest.php index 7f98f21d1e2bb..2f909c5b7cd92 100644 --- a/app/code/Magento/Webapi/Test/Unit/Controller/Rest/ParamsOverriderTest.php +++ b/app/code/Magento/Webapi/Test/Unit/Controller/Rest/ParamsOverriderTest.php @@ -7,6 +7,9 @@ namespace Magento\Webapi\Test\Unit\Controller\Rest; use \Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\Api\SimpleDataObjectConverter; +use Magento\Webapi\Controller\Rest\ParamsOverrider; +use PHPUnit\Framework\MockObject\MockObject; /** * Test Magento\Webapi\Controller\Rest\ParamsOverrider @@ -36,10 +39,31 @@ public function testOverrideParams($requestData, $parameters, $expectedOverridde ['userContext' => $userContextMock] ); - /** @var \Magento\Webapi\Controller\Rest\ParamsOverrider $paramsOverrider */ + /** @var MockObject $objectConverter */ + $objectConverter = $this->getMockBuilder(SimpleDataObjectConverter::class) + ->disableOriginalConstructor() + ->setMethods(['convertKeysToCamelCase']) + ->getMock(); + $objectConverter->expects($this->any()) + ->method('convertKeysToCamelCase') + ->willReturnCallback( + function (array $array) { + $converted = []; + foreach ($array as $key => $value) { + $converted[mb_strtolower($key)] = $value; + } + + return $converted; + } + ); + + /** @var ParamsOverrider $paramsOverrider */ $paramsOverrider = $objectManager->getObject( - \Magento\Webapi\Controller\Rest\ParamsOverrider::class, - ['paramOverriders' => ['%customer_id%' => $paramOverriderCustomerId ]] + ParamsOverrider::class, + [ + 'paramOverriders' => ['%customer_id%' => $paramOverriderCustomerId ], + 'dataObjectConverter' => $objectConverter + ] ); $this->assertEquals($expectedOverriddenParams, $paramsOverrider->override($requestData, $parameters)); diff --git a/app/code/Magento/Webapi/Test/Unit/Controller/Soap/Request/HandlerTest.php b/app/code/Magento/Webapi/Test/Unit/Controller/Soap/Request/HandlerTest.php deleted file mode 100644 index 4094811b1386e..0000000000000 --- a/app/code/Magento/Webapi/Test/Unit/Controller/Soap/Request/HandlerTest.php +++ /dev/null @@ -1,127 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Webapi\Test\Unit\Controller\Soap\Request; - -use Magento\Framework\Api\SimpleDataObjectConverter; -use Magento\Webapi\Model\ServiceMetadata; - -/** - * Test for \Magento\Webapi\Controller\Soap\Request\Handler. - */ -class HandlerTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\Webapi\Controller\Soap\Request\Handler */ - protected $_handler; - - /** @var \Magento\Framework\ObjectManagerInterface */ - protected $_objectManagerMock; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_apiConfigMock; - - /** @var \Magento\Framework\Webapi\Request */ - protected $_requestMock; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_authorizationMock; - - /** @var SimpleDataObjectConverter|\PHPUnit_Framework_MockObject_MockObject */ - protected $_dataObjectConverter; - - /** @var \Magento\Framework\Webapi\ServiceInputProcessor|\PHPUnit_Framework_MockObject_MockObject */ - protected $_serviceInputProcessorMock; - - /** @var \Magento\Framework\Reflection\DataObjectProcessor|\PHPUnit_Framework_MockObject_MockObject */ - protected $_dataObjectProcessorMock; - - /** @var \Magento\Framework\Reflection\MethodsMap|\PHPUnit_Framework_MockObject_MockObject */ - protected $_methodsMapProcessorMock; - - /** @var array */ - protected $_arguments; - - protected function setUp() - { - /** Prepare mocks for SUT constructor. */ - $this->_apiConfigMock = $this->getMockBuilder(\Magento\Webapi\Model\Soap\Config::class) - ->setMethods(['getServiceMethodInfo'])->disableOriginalConstructor()->getMock(); - $this->_requestMock = $this->createMock(\Magento\Framework\Webapi\Request::class); - $this->_objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $this->_authorizationMock = $this->createMock(\Magento\Framework\Webapi\Authorization::class); - $this->_dataObjectConverter = $this->createPartialMock( - \Magento\Framework\Api\SimpleDataObjectConverter::class, - ['convertStdObjectToArray'] - ); - $this->_serviceInputProcessorMock = $this->createMock(\Magento\Framework\Webapi\ServiceInputProcessor::class); - $this->_dataObjectProcessorMock = $this->createMock(\Magento\Framework\Reflection\DataObjectProcessor::class); - $this->_methodsMapProcessorMock = $this->createMock(\Magento\Framework\Reflection\MethodsMap::class); - - /** Initialize SUT. */ - $this->_handler = new \Magento\Webapi\Controller\Soap\Request\Handler( - $this->_requestMock, - $this->_objectManagerMock, - $this->_apiConfigMock, - $this->_authorizationMock, - $this->_dataObjectConverter, - $this->_serviceInputProcessorMock, - $this->_dataObjectProcessorMock, - $this->_methodsMapProcessorMock - ); - parent::setUp(); - } - - public function testCall() - { - $requestedServices = ['requestedServices']; - $this->_requestMock->expects($this->once()) - ->method('getRequestedServices') - ->will($this->returnValue($requestedServices)); - $this->_dataObjectConverter->expects($this->once()) - ->method('convertStdObjectToArray') - ->will($this->returnValue(['field' => 1])); - $this->_methodsMapProcessorMock->method('getMethodReturnType')->willReturn('string'); - $operationName = 'soapOperation'; - $className = \Magento\Framework\DataObject::class; - $methodName = 'testMethod'; - $isSecure = false; - $aclResources = [['Magento_TestModule::resourceA']]; - $this->_apiConfigMock->expects($this->once()) - ->method('getServiceMethodInfo') - ->with($operationName, $requestedServices) - ->will( - $this->returnValue( - [ - ServiceMetadata::KEY_CLASS => $className, - ServiceMetadata::KEY_METHOD => $methodName, - ServiceMetadata::KEY_IS_SECURE => $isSecure, - ServiceMetadata::KEY_ACL_RESOURCES => $aclResources, - ] - ) - ); - - $this->_authorizationMock->expects($this->once())->method('isAllowed')->will($this->returnValue(true)); - $serviceMock = $this->getMockBuilder($className) - ->disableOriginalConstructor() - ->setMethods([$methodName]) - ->getMock(); - - $serviceResponse = ['foo' => 'bar']; - $serviceMock->expects($this->once())->method($methodName)->will($this->returnValue($serviceResponse)); - $this->_objectManagerMock->expects($this->once())->method('get')->with($className) - ->will($this->returnValue($serviceMock)); - $this->_serviceInputProcessorMock - ->expects($this->once()) - ->method('process') - ->will($this->returnArgument(2)); - - /** Execute SUT. */ - $this->assertEquals( - ['result' => $serviceResponse], - $this->_handler->__call($operationName, [(object)['field' => 1]]) - ); - } -} diff --git a/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.php b/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.php index 2df8697b19857..49c794bf773b9 100644 --- a/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.php +++ b/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.php @@ -13,13 +13,29 @@ 'Magento_Customer::read', ], 'secure' => false, + 'realMethod' => 'getById', + 'parameters' => [] ], 'save' => [ 'resources' => [ - 'Magento_Customer::customer_self', 'Magento_Customer::manage' ], + 'secure' => false, + 'realMethod' => 'save', + 'parameters' => [] + ], + 'saveSelf' => [ + 'resources' => [ + 'Magento_Customer::customer_self' + ], 'secure' => true, + 'realMethod' => 'save', + 'parameters' => [ + 'id' => [ + 'force' => false, + 'value' => null, + ], + ], ], 'deleteById' => [ 'resources' => [ @@ -27,6 +43,8 @@ 'Magento_Customer::delete', ], 'secure' => false, + 'realMethod' => 'deleteById', + 'parameters' => [] ], ], ], diff --git a/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.xml b/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.xml index b08b3087bfc1f..50b9abb8f17ae 100644 --- a/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.xml +++ b/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.xml @@ -25,7 +25,7 @@ <parameter name="id" force="true">%customer_id%</parameter> </data> </route> - <route url="/V1/customers/me" method="PUT" secure="true"> + <route url="/V1/customers/me" method="PUT" secure="true" soapOperation="saveSelf"> <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="save" /> <resources> <resource ref="Magento_Customer::customer_self" /> diff --git a/app/code/Magento/Webapi/Test/Unit/Model/ServiceMetadataTest.php b/app/code/Magento/Webapi/Test/Unit/Model/ServiceMetadataTest.php deleted file mode 100644 index a24878c408e13..0000000000000 --- a/app/code/Magento/Webapi/Test/Unit/Model/ServiceMetadataTest.php +++ /dev/null @@ -1,469 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Webapi\Test\Unit\Model; - -use Magento\Framework\Serialize\SerializerInterface; -use Magento\Webapi\Model\Config; -use Magento\Webapi\Model\Cache\Type\Webapi; -use Magento\Webapi\Model\Config\ClassReflector; -use Magento\Framework\Reflection\TypeProcessor; -use Magento\Webapi\Model\ServiceMetadata; -use Magento\Customer\Api\CustomerRepositoryInterface; - -class ServiceMetadataTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ServiceMetadata - */ - private $serviceMetadata; - - /** - * @var Webapi|\PHPUnit_Framework_MockObject_MockObject - */ - private $cacheMock; - - /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject - */ - private $configMock; - - /** - * @var ClassReflector|\PHPUnit_Framework_MockObject_MockObject - */ - private $classReflectorMock; - - /** - * @var TypeProcessor|\PHPUnit_Framework_MockObject_MockObject - */ - private $typeProcessorMock; - - /** - * @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $serializerMock; - - protected function setUp() - { - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->configMock = $this->createMock(Config::class); - $this->cacheMock = $this->createMock(Webapi::class); - $this->classReflectorMock = $this->createMock(ClassReflector::class); - $this->typeProcessorMock = $this->createMock(TypeProcessor::class); - $this->serializerMock = $this->createMock(SerializerInterface::class); - - $this->serviceMetadata = $objectManager->getObject( - ServiceMetadata::class, - [ - 'config' => $this->configMock, - 'cache' => $this->cacheMock, - 'classReflector' => $this->classReflectorMock, - 'typeProcessor' => $this->typeProcessorMock, - 'serializer' => $this->serializerMock - ] - ); - } - - public function testGetServicesConfig() - { - $servicesConfig = ['foo' => 'bar']; - $typeData = ['bar' => 'foo']; - $serializedServicesConfig = 'serialized services config'; - $serializedTypeData = 'serialized type data'; - $this->cacheMock->expects($this->at(0)) - ->method('load') - ->with(ServiceMetadata::SERVICES_CONFIG_CACHE_ID) - ->willReturn($serializedServicesConfig); - $this->cacheMock->expects($this->at(1)) - ->method('load') - ->with(ServiceMetadata::REFLECTED_TYPES_CACHE_ID) - ->willReturn($serializedTypeData); - $this->serializerMock->expects($this->at(0)) - ->method('unserialize') - ->with($serializedServicesConfig) - ->willReturn($servicesConfig); - $this->serializerMock->expects($this->at(1)) - ->method('unserialize') - ->with($serializedTypeData) - ->willReturn($typeData); - $this->typeProcessorMock->expects($this->once()) - ->method('setTypesData') - ->with($typeData); - $this->serviceMetadata->getServicesConfig(); - $this->assertEquals($servicesConfig, $this->serviceMetadata->getServicesConfig()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testGetServicesConfigNoCache() - { - $servicesConfig = [ - 'services' => [ - CustomerRepositoryInterface::class => [ - 'V1' => [ - 'methods' => [ - 'getById' => [ - 'resources' => [ - [ - 'Magento_Customer::customer', - ] - ], - 'secure' => false - ] - ] - ] - ] - ] - ]; - $methodsReflectionData = [ - 'getById' => [ - 'documentation' => 'Get customer by customer ID.', - 'interface' => [ - 'in' => [ - 'parameters' => [ - 'customerId' => [ - 'type' => 'int', - 'required' => true, - 'documentation' => null - ] - ] - ], - 'out' => [ - 'parameters' => [ - 'result' => [ - 'type' => 'CustomerDataCustomerInterface', - 'required' => true, - 'documentation' => null - ] - ] - ] - ] - ] - ]; - $servicesMetadata = [ - 'customerCustomerRepositoryV1' => [ - 'methods' => array_merge_recursive( - [ - 'getById' => [ - 'resources' => [ - [ - 'Magento_Customer::customer', - ], - ], - 'method' => 'getById', - 'inputRequired' => false, - 'isSecure' => false, - ] - ], - $methodsReflectionData - ), - 'class' => CustomerRepositoryInterface::class, - 'description' => 'Customer CRUD interface.' - ] - ]; - $typeData = [ - 'CustomerDataCustomerInterface' => [ - 'documentation' => 'Customer interface.', - 'parameters' => [ - 'id' => [ - 'type' => 'int', - 'required' => false, - 'documentation' => 'Customer id' - ] - ] - ] - ]; - $serializedServicesConfig = 'serialized services config'; - $serializedTypeData = 'serialized type data'; - $this->cacheMock->expects($this->at(0)) - ->method('load') - ->with(ServiceMetadata::SERVICES_CONFIG_CACHE_ID) - ->willReturn(false); - $this->cacheMock->expects($this->at(1)) - ->method('load') - ->with(ServiceMetadata::REFLECTED_TYPES_CACHE_ID) - ->willReturn(false); - $this->serializerMock->expects($this->never()) - ->method('unserialize'); - $this->configMock->expects($this->once()) - ->method('getServices') - ->willReturn($servicesConfig); - $this->classReflectorMock->expects($this->once()) - ->method('reflectClassMethods') - ->willReturn($methodsReflectionData); - $this->classReflectorMock->expects($this->once()) - ->method('extractClassDescription') - ->with(CustomerRepositoryInterface::class) - ->willReturn('Customer CRUD interface.'); - $this->typeProcessorMock->expects($this->once()) - ->method('getTypesData') - ->willReturn($typeData); - $this->serializerMock->expects($this->at(0)) - ->method('serialize') - ->with($servicesMetadata) - ->willReturn($serializedServicesConfig); - $this->serializerMock->expects($this->at(1)) - ->method('serialize') - ->with($typeData) - ->willReturn($serializedTypeData); - $this->cacheMock->expects($this->at(2)) - ->method('save') - ->with( - $serializedServicesConfig, - ServiceMetadata::SERVICES_CONFIG_CACHE_ID - ); - $this->cacheMock->expects($this->at(3)) - ->method('save') - ->with( - $serializedTypeData, - ServiceMetadata::REFLECTED_TYPES_CACHE_ID - ); - $this->serviceMetadata->getServicesConfig(); - $this->assertEquals($servicesMetadata, $this->serviceMetadata->getServicesConfig()); - } - - public function testGetRoutesConfig() - { - $routesConfig = ['foo' => 'bar']; - $typeData = ['bar' => 'foo']; - $serializedRoutesConfig = 'serialized routes config'; - $serializedTypeData = 'serialized type data'; - $this->cacheMock->expects($this->at(0)) - ->method('load') - ->with(ServiceMetadata::ROUTES_CONFIG_CACHE_ID) - ->willReturn($serializedRoutesConfig); - $this->cacheMock->expects($this->at(1)) - ->method('load') - ->with(ServiceMetadata::REFLECTED_TYPES_CACHE_ID) - ->willReturn($serializedTypeData); - $this->serializerMock->expects($this->at(0)) - ->method('unserialize') - ->with($serializedRoutesConfig) - ->willReturn($routesConfig); - $this->serializerMock->expects($this->at(1)) - ->method('unserialize') - ->with($serializedTypeData) - ->willReturn($typeData); - $this->typeProcessorMock->expects($this->once()) - ->method('setTypesData') - ->with($typeData); - $this->serviceMetadata->getRoutesConfig(); - $this->assertEquals($routesConfig, $this->serviceMetadata->getRoutesConfig()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testGetRoutesConfigNoCache() - { - $servicesConfig = [ - 'services' => [ - CustomerRepositoryInterface::class => [ - 'V1' => [ - 'methods' => [ - 'getById' => [ - 'resources' => [ - [ - 'Magento_Customer::customer', - ] - ], - 'secure' => false - ] - ] - ] - ] - ], - 'routes' => [ - '/V1/customers/:customerId' => [ - 'GET' => [ - 'secure' => false, - 'service' => [ - 'class' => CustomerRepositoryInterface::class, - 'method' => 'getById' - ], - 'resources' => [ - 'Magento_Customer::customer' => true - ], - 'parameters' => [] - ] - ] - ], - 'class' => CustomerRepositoryInterface::class, - 'description' => 'Customer CRUD interface.', - ]; - $methodsReflectionData = [ - 'getById' => [ - 'documentation' => 'Get customer by customer ID.', - 'interface' => [ - 'in' => [ - 'parameters' => [ - 'customerId' => [ - 'type' => 'int', - 'required' => true, - 'documentation' => null - ] - ] - ], - 'out' => [ - 'parameters' => [ - 'result' => [ - 'type' => 'CustomerDataCustomerInterface', - 'required' => true, - 'documentation' => null - ] - ] - ] - ] - ] - ]; - $routesMetadata = [ - 'customerCustomerRepositoryV1' => [ - 'methods' => array_merge_recursive( - [ - 'getById' => [ - 'resources' => [ - [ - 'Magento_Customer::customer', - ] - ], - 'method' => 'getById', - 'inputRequired' => false, - 'isSecure' => false, - ] - ], - $methodsReflectionData - ), - 'routes' => [ - '/V1/customers/:customerId' => [ - 'GET' => [ - 'method' => 'getById', - 'parameters' => [] - ] - ] - ], - 'class' => CustomerRepositoryInterface::class, - 'description' => 'Customer CRUD interface.' - ] - ]; - $typeData = [ - 'CustomerDataCustomerInterface' => [ - 'documentation' => 'Customer interface.', - 'parameters' => [ - 'id' => [ - 'type' => 'int', - 'required' => false, - 'documentation' => 'Customer id' - ] - ] - ] - ]; - $serializedRoutesConfig = 'serialized routes config'; - $serializedTypeData = 'serialized type data'; - $this->cacheMock->expects($this->at(0)) - ->method('load') - ->with(ServiceMetadata::ROUTES_CONFIG_CACHE_ID) - ->willReturn(false); - $this->cacheMock->expects($this->at(1)) - ->method('load') - ->with(ServiceMetadata::REFLECTED_TYPES_CACHE_ID) - ->willReturn(false); - $this->serializerMock->expects($this->never()) - ->method('unserialize'); - $this->configMock->expects($this->exactly(2)) - ->method('getServices') - ->willReturn($servicesConfig); - $this->classReflectorMock->expects($this->once()) - ->method('reflectClassMethods') - ->willReturn($methodsReflectionData); - $this->classReflectorMock->expects($this->once()) - ->method('extractClassDescription') - ->with(CustomerRepositoryInterface::class) - ->willReturn('Customer CRUD interface.'); - $this->typeProcessorMock->expects($this->exactly(2)) - ->method('getTypesData') - ->willReturn($typeData); - $this->serializerMock->expects($this->at(2)) - ->method('serialize') - ->with($routesMetadata) - ->willReturn($serializedRoutesConfig); - $this->serializerMock->expects($this->at(3)) - ->method('serialize') - ->with($typeData) - ->willReturn($serializedTypeData); - $this->cacheMock->expects($this->at(6)) - ->method('save') - ->with( - $serializedRoutesConfig, - ServiceMetadata::ROUTES_CONFIG_CACHE_ID - ); - $this->cacheMock->expects($this->at(7)) - ->method('save') - ->with( - $serializedTypeData, - ServiceMetadata::REFLECTED_TYPES_CACHE_ID - ); - $this->serviceMetadata->getRoutesConfig(); - $this->assertEquals($routesMetadata, $this->serviceMetadata->getRoutesConfig()); - } - - /** - * @dataProvider getServiceNameDataProvider - */ - public function testGetServiceName($className, $version, $preserveVersion, $expected) - { - $this->assertEquals( - $expected, - $this->serviceMetadata->getServiceName($className, $version, $preserveVersion) - ); - } - - /** - * @return string - */ - public function getServiceNameDataProvider() - { - return [ - [ - \Magento\Customer\Api\AccountManagementInterface::class, - 'V1', - false, - 'customerAccountManagement' - ], - [ - \Magento\Customer\Api\AddressRepositoryInterface::class, - 'V1', - true, - 'customerAddressRepositoryV1' - ], - ]; - } - - /** - * @expectedException \InvalidArgumentException - * @dataProvider getServiceNameInvalidNameDataProvider - */ - public function testGetServiceNameInvalidName($interfaceClassName, $version) - { - $this->serviceMetadata->getServiceName($interfaceClassName, $version); - } - - /** - * @return string - */ - public function getServiceNameInvalidNameDataProvider() - { - return [ - ['BarV1Interface', 'V1'], // Missed vendor, module and Service - ['Service\\V1Interface', 'V1'], // Missed vendor and module - ['Magento\\Foo\\Service\\BarVxInterface', 'V1'], // Version number should be a number - ['Magento\\Foo\\Service\\BarInterface', 'V1'], // Missed version - ['Magento\\Foo\\Service\\BarV1', 'V1'], // Missed Interface - ['Foo\\Service\\BarV1Interface', 'V1'], // Missed module - ['Foo\\BarV1Interface', 'V1'] // Missed module and Service - ]; - } -} diff --git a/app/code/Magento/Webapi/etc/di.xml b/app/code/Magento/Webapi/etc/di.xml index 62ba22a658aaa..a08bb04a39433 100644 --- a/app/code/Magento/Webapi/etc/di.xml +++ b/app/code/Magento/Webapi/etc/di.xml @@ -58,4 +58,11 @@ </argument> </arguments> </type> + <type name="Magento\Webapi\Model\Config\Reader"> + <arguments> + <argument name="converter" xsi:type="object">Magento\Webapi\Model\Config\Converter</argument> + <argument name="schemaLocator" xsi:type="object">Magento\Webapi\Model\Config\SchemaLocator</argument> + <argument name="fileName" xsi:type="string">webapi.xml</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Webapi/etc/webapi.xsd b/app/code/Magento/Webapi/etc/webapi.xsd index 7a237cba2cfd2..40962d6d0da2e 100644 --- a/app/code/Magento/Webapi/etc/webapi.xsd +++ b/app/code/Magento/Webapi/etc/webapi.xsd @@ -8,68 +8,17 @@ */ --> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> - <xs:element name="routes" type="routesType"/> - - <xs:complexType name="routesType"> - <xs:sequence> - <xs:element name="route" type="routeType" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - - <xs:complexType name="routeType"> - <xs:sequence> - <xs:element name="service" type="serviceType"/> - <xs:element name="resources" type="resourcesType"/> - <xs:element name="data" type="dataType" minOccurs="0"/> - </xs:sequence> - <xs:attribute name="method" use="required"> - <xs:simpleType> - <xs:restriction base="xs:string"> - <xs:enumeration value="GET"/> - <xs:enumeration value="PUT"/> - <xs:enumeration value="POST"/> - <xs:enumeration value="DELETE"/> - </xs:restriction> - </xs:simpleType> - </xs:attribute> - <xs:attribute name="url" type="xs:string" use="required"/> - <xs:attribute name="secure" type="xs:boolean"/> - </xs:complexType> - - <xs:complexType name="serviceType"> - <xs:attribute name="class" type="xs:string" use="required"/> - <xs:attribute name="method" type="xs:string" use="required"/> - </xs:complexType> - - <xs:complexType name="resourcesType" > - <xs:sequence> - <xs:element name="resource" type="resourceType" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - - <xs:complexType name="resourceType"> - <xs:attribute name="ref" use="required"> - <xs:simpleType> - <xs:restriction base="xs:string"> - <xs:pattern value=".+(, ?.+)*"/> - </xs:restriction> - </xs:simpleType> - </xs:attribute> - </xs:complexType> - - <xs:complexType name="dataType" > - <xs:sequence> - <xs:element name="parameter" type="parameterType" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - - <xs:complexType name="parameterType"> - <xs:simpleContent> - <xs:extension base="xs:string"> - <xs:attribute name="name" type="xs:string" use="required"/> - <xs:attribute name="force" type="xs:boolean"/> - </xs:extension> - </xs:simpleContent> - </xs:complexType> - + <xs:redefine schemaLocation="urn:magento:module:Magento_Webapi:etc/webapi_base.xsd"> + <xs:complexType name="routeType"> + <xs:complexContent> + <xs:extension base="routeType"> + <xs:sequence> + <xs:element name="service" type="serviceType" minOccurs="0"/> + <xs:element name="resources" type="resourcesType" minOccurs="0"/> + <xs:element name="data" type="dataType" minOccurs="0"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:redefine> </xs:schema> diff --git a/app/code/Magento/Webapi/etc/webapi_base.xsd b/app/code/Magento/Webapi/etc/webapi_base.xsd new file mode 100644 index 0000000000000..7d1a5a14ba78f --- /dev/null +++ b/app/code/Magento/Webapi/etc/webapi_base.xsd @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Structure description for webapi.xml configuration files. + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="routes" type="routesType"/> + + <xs:complexType name="routesType"> + <xs:sequence> + <xs:element name="route" type="routeType" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="routeType"> + <xs:attribute name="method" use="required"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:enumeration value="GET"/> + <xs:enumeration value="PUT"/> + <xs:enumeration value="POST"/> + <xs:enumeration value="DELETE"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="url" type="xs:string" use="required"/> + <xs:attribute name="secure" type="xs:boolean"/> + <xs:attribute name="soapOperation" type="xs:string"/> + </xs:complexType> + + <xs:complexType name="serviceType"> + <xs:attribute name="class" type="xs:string" use="required"/> + <xs:attribute name="method" type="xs:string" use="required"/> + </xs:complexType> + + <xs:complexType name="resourcesType" > + <xs:sequence> + <xs:element name="resource" type="resourceType" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="resourceType"> + <xs:attribute name="ref" use="required"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:pattern value=".+(, ?.+)*"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + </xs:complexType> + + <xs:complexType name="dataType" > + <xs:sequence> + <xs:element name="parameter" type="parameterType" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="parameterType"> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute name="name" type="xs:string" use="required"/> + <xs:attribute name="force" type="xs:boolean"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + +</xs:schema> diff --git a/app/code/Magento/Webapi/etc/webapi_merged.xsd b/app/code/Magento/Webapi/etc/webapi_merged.xsd new file mode 100644 index 0000000000000..b775d5bbac609 --- /dev/null +++ b/app/code/Magento/Webapi/etc/webapi_merged.xsd @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Structure description for webapi.xml configuration files. + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:redefine schemaLocation="urn:magento:module:Magento_Webapi:etc/webapi_base.xsd"> + <xs:complexType name="routeType"> + <xs:complexContent> + <xs:extension base="routeType"> + <xs:sequence> + <xs:element name="service" type="serviceType"/> + <xs:element name="resources" type="resourcesType"/> + <xs:element name="data" type="dataType" minOccurs="0"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + </xs:redefine> +</xs:schema> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml new file mode 100644 index 0000000000000..21fa334a43196 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySecureURLRedirectWishlist"> + <annotations> + <features value="Wishlist"/> + <stories value="Storefront Secure URLs"/> + <title value="Verify Secure URLs For Storefront Wishlist Pages"/> + <description value="Verify that the Secure URL configuration applies to the Wishlist pages on the Storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15543"/> + <group value="wishlist"/> + <group value="configuration"/> + <group value="secure_storefront_url"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <executeJS function="return window.location.host" stepKey="hostname"/> + <magentoCLI command="config:set web/secure/base_url https://{$hostname}/" stepKey="setSecureBaseURL"/> + <magentoCLI command="config:set web/secure/use_in_frontend 1" stepKey="useSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + <executeJS function="return window.location.host" stepKey="hostname"/> + <amOnUrl url="http://{$hostname}/wishlist" stepKey="goToUnsecureWishlistURL"/> + <seeCurrentUrlEquals url="https://{$hostname}/wishlist" stepKey="seeSecureWishlistURL"/> + </test> +</tests> 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 66c9086c15aa7..08aeb35d7adb2 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 @@ -545,6 +545,7 @@ & > .admin__field-label { #mix-grid .column(@field-label-grid__column, @field-grid__columns); cursor: pointer; + background: @color-white; left: 0; position: absolute; top: 0; diff --git a/composer.lock b/composer.lock index 413e700c87b15..6b1eaee8f25e6 100644 --- a/composer.lock +++ b/composer.lock @@ -2395,7 +2395,7 @@ }, { "name": "Gert de Pagter", - "email": "backendtea@gmail.com" + "email": "BackEndTea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/Webapi/Adapter/Soap.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/Webapi/Adapter/Soap.php index e2e32c119af21..8453edb071b3e 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/Webapi/Adapter/Soap.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/Webapi/Adapter/Soap.php @@ -21,7 +21,7 @@ class Soap implements \Magento\TestFramework\TestCase\Webapi\AdapterInterface * * @var \Zend\Soap\Client[] */ - protected $_soapClients = []; + protected $_soapClients = ['custom' => [], 'default' => []]; /** * @var \Magento\Webapi\Model\Soap\Config @@ -46,7 +46,7 @@ public function __construct() } /** - * {@inheritdoc} + * @inheritdoc */ public function call($serviceInfo, $arguments = [], $storeCode = null, $integration = null) { @@ -75,12 +75,28 @@ protected function _getSoapClient($serviceInfo, $storeCode = null) [$this->_getSoapServiceName($serviceInfo) . $this->_getSoapServiceVersion($serviceInfo)], $storeCode ); - /** Check if there is SOAP client initialized with requested WSDL available */ - if (!isset($this->_soapClients[$wsdlUrl])) { - $token = isset($serviceInfo['soap']['token']) ? $serviceInfo['soap']['token'] : null; - $this->_soapClients[$wsdlUrl] = $this->instantiateSoapClient($wsdlUrl, $token); + /** @var \Zend\Soap\Client $soapClient */ + $soapClient = null; + if (isset($serviceInfo['soap']['token'])) { + $token = $serviceInfo['soap']['token']; + if (array_key_exists($token, $this->_soapClients['custom']) + && array_key_exists($wsdlUrl, $this->_soapClients['custom'][$token]) + ) { + $soapClient = $this->_soapClients['custom'][$token][$wsdlUrl]; + } else { + if (!array_key_exists($token, $this->_soapClients['custom'])) { + $this->_soapClients['custom'][$token] = []; + } + $soapClient = $this->_soapClients['custom'][$token][$wsdlUrl] + = $this->instantiateSoapClient($wsdlUrl, $token); + } + } else { + if (!isset($this->_soapClients[$wsdlUrl])) { + $this->_soapClients['default'][$wsdlUrl] = $this->instantiateSoapClient($wsdlUrl, null); + } + $soapClient = $this->_soapClients['default'][$wsdlUrl]; } - return $this->_soapClients[$wsdlUrl]; + return $soapClient; } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementMeTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementMeTest.php index e85523cf40ea6..31894c1332ad5 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementMeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementMeTest.php @@ -8,6 +8,7 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\CustomerRegistry; +use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Integration\Model\Oauth\Token as TokenModel; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Customer as CustomerHelper; @@ -23,6 +24,9 @@ class AccountManagementMeTest extends \Magento\TestFramework\TestCase\WebapiAbst { const RESOURCE_PATH = '/V1/customers/me'; const RESOURCE_PATH_CUSTOMER_TOKEN = "/V1/integration/customer/token"; + const REPO_SERVICE = 'customerCustomerRepositoryV1'; + const ACCOUNT_SERVICE = 'customerAccountManagementV1'; + const SERVICE_VERSION = 'V1'; /** * @var CustomerRepositoryInterface @@ -59,13 +63,16 @@ class AccountManagementMeTest extends \Magento\TestFramework\TestCase\WebapiAbst */ private $dataObjectProcessor; + /** + * @var CustomerTokenServiceInterface + */ + private $tokenService; + /** * Execute per test initialization. */ public function setUp() { - $this->_markTestAsRestOnly(); - $this->customerRegistry = Bootstrap::getObjectManager()->get( \Magento\Customer\Model\CustomerRegistry::class ); @@ -80,6 +87,7 @@ public function setUp() $this->customerHelper = new CustomerHelper(); $this->customerData = $this->customerHelper->createSampleCustomer(); + $this->tokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); // get token $this->resetTokenForCustomerSampleData(); @@ -114,8 +122,17 @@ public function testChangePassword() 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, 'token' => $this->token, ], + 'soap' => [ + 'service' => self::ACCOUNT_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::ACCOUNT_SERVICE .'ChangePasswordById', + 'token' => $this->token + ] ]; $requestData = ['currentPassword' => 'test@123', 'newPassword' => '123@test']; + if (TESTS_WEB_API_ADAPTER === 'soap') { + $requestData['customerId'] = 0; + } $this->assertTrue($this->_webApiCall($serviceInfo, $requestData)); $customerResponseData = $this->customerAccountManagement @@ -141,6 +158,12 @@ public function testUpdateCustomer() 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, 'token' => $this->token, ], + 'soap' => [ + 'service' => self::REPO_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::REPO_SERVICE .'SaveSelf', + 'token' => $this->token + ] ]; $requestData = ['customer' => $updatedCustomerData]; @@ -171,8 +194,18 @@ public function testGetCustomerData() 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, 'token' => $this->token, ], + 'soap' => [ + 'service' => self::REPO_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::REPO_SERVICE .'GetSelf', + 'token' => $this->token + ] ]; - $customerDetailsResponse = $this->_webApiCall($serviceInfo); + $arguments = []; + if (TESTS_WEB_API_ADAPTER === 'soap') { + $arguments['customerId'] = 0; + } + $customerDetailsResponse = $this->_webApiCall($serviceInfo, $arguments); unset($expectedCustomerDetails['custom_attributes']); unset($customerDetailsResponse['custom_attributes']); //for REST @@ -188,8 +221,17 @@ public function testGetCustomerActivateCustomer() 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, 'token' => $this->token, ], + 'soap' => [ + 'service' => self::ACCOUNT_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::ACCOUNT_SERVICE .'ActivateById', + 'token' => $this->token + ] ]; $requestData = ['confirmationKey' => $this->customerData[CustomerInterface::CONFIRMATION]]; + if (TESTS_WEB_API_ADAPTER === 'soap') { + $requestData['customerId'] = 0; + } $customerResponseData = $this->_webApiCall($serviceInfo, $requestData); $this->assertEquals($this->customerData[CustomerInterface::ID], $customerResponseData[CustomerInterface::ID]); // Confirmation key is removed after confirmation @@ -220,6 +262,12 @@ public function testGetDefaultBillingAddress() 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, 'token' => $this->token, ], + 'soap' => [ + 'service' => self::ACCOUNT_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::ACCOUNT_SERVICE .'GetMyDefaultBillingAddress', + 'token' => $this->token + ] ]; $requestData = ['customerId' => $fixtureCustomerId]; $addressData = $this->_webApiCall($serviceInfo, $requestData); @@ -241,6 +289,12 @@ public function testGetDefaultShippingAddress() 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, 'token' => $this->token, ], + 'soap' => [ + 'service' => self::ACCOUNT_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::ACCOUNT_SERVICE .'GetMyDefaultShippingAddress', + 'token' => $this->token + ] ]; $requestData = ['customerId' => $fixtureCustomerId]; $addressData = $this->_webApiCall($serviceInfo, $requestData); @@ -324,14 +378,7 @@ protected function resetTokenForCustomerSampleData() */ protected function resetTokenForCustomer($username, $password) { - // get customer ID token - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, - ], - ]; - $requestData = ['username' => $username, 'password' => $password]; - $this->token = $this->_webApiCall($serviceInfo, $requestData); + $this->token = $this->tokenService->createCustomerAccessToken($username, $password); + $this->customerRegistry->remove($this->customerRepository->get($username)->getId()); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php index 8f84f485fc2ae..709abbbb8fbf9 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php @@ -147,12 +147,9 @@ public function tearDown() * Validate update by invalid customer. * * @expectedException \Exception - * @expectedExceptionMessage The consumer isn't authorized to access %resources. */ public function testInvalidCustomerUpdate() { - $this->_markTestAsRestOnly(); - //Create first customer and retrieve customer token. $firstCustomerData = $this->_createCustomer(); @@ -181,6 +178,12 @@ public function testInvalidCustomerUpdate() 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, 'token' => $token, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + 'token' => $token ] ]; diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/GroupRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/GroupRepositoryTest.php index 999a2daa26065..920f1f2c428a5 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/GroupRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/GroupRepositoryTest.php @@ -55,22 +55,6 @@ public function setUp() $this->customerGroupFactory = $objectManager->create(\Magento\Customer\Api\Data\GroupInterfaceFactory::class); } - /** - * Execute per test cleanup. - */ - public function tearDown() - { - parent::tearDown(); - } - - /** - * Cleaning up the extra groups that might have been created as part of the testing. - */ - public static function tearDownAfterClass() - { - parent::tearDownAfterClass(); - } - /** * Verify the retrieval of a customer group by Id. * @@ -874,7 +858,7 @@ public function testSearchGroupsDataProvider() return [ ['tax_class_id', 3, []], ['tax_class_id', 0, null], - ['code', md5(mt_rand(0, 10000000000) . time()), null], + ['code', hash("sha256", random_int(0, 10000000000) . time()), null], [ 'id', 0, diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php index 49e2a9dd53999..f39feabccd839 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php @@ -48,6 +48,40 @@ public function testProductSmallImageUrlWithExistingImage() self::assertTrue($this->checkImageExists($response['products']['items'][0]['small_image']['url'])); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_with_multiple_images.php + */ + public function testMediaGalleryTypesAreCorrect() + { + $productSku = 'simple'; + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + media_gallery_entries { + label + media_type + file + types + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'][0]['media_gallery_entries']); + $mediaGallery = $response['products']['items'][0]['media_gallery_entries']; + $this->assertCount(2, $mediaGallery); + $this->assertEquals('Image Alt Text', $mediaGallery[0]['label']); + $this->assertEquals('image', $mediaGallery[0]['media_type']); + $this->assertContains('magento_image', $mediaGallery[0]['file']); + $this->assertEquals(['image', 'small_image'], $mediaGallery[0]['types']); + $this->assertEquals('Thumbnail Image', $mediaGallery[1]['label']); + $this->assertEquals('image', $mediaGallery[1]['media_type']); + $this->assertContains('magento_thumbnail', $mediaGallery[1]['file']); + $this->assertEquals(['thumbnail', 'swatch_image'], $mediaGallery[1]['types']); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductQueryTest.php new file mode 100644 index 0000000000000..6de803c19e699 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductQueryTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test configurable product queries work correctly + */ +class ConfigurableProductQueryTest extends GraphQlAbstract +{ + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ + public function testNonVisibleVariationsNotReturned() + { + $categoryId = '2'; + $query = <<<QUERY +{ + products(filter: {category_id: {eq: "{$categoryId}"}}) { + items { + __typename + sku + name + url_key + price { + regularPrice { + amount { + currency + value + } + } + } + media_gallery_entries { + media_type + label + position + file + id + types + } + description { + html + } + } + } +} +QUERY; + + $result = $this->graphQlQuery($query); + $products = $result['products']['items']; + $this->assertCount(1, $products); + $this->assertEquals('ConfigurableProduct', $products[0]['__typename']); + $this->assertEquals('configurable', $products[0]['sku']); + $this->assertArrayHasKey('media_gallery_entries', $products[0]); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php index 85c2fc1c847b4..1388f745783a6 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php @@ -120,4 +120,54 @@ private function getQuery(string $email, string $password) : string } MUTATION; } + + /** + * Verify customer with empty email + */ + public function testGenerateCustomerTokenWithEmptyEmail() + { + $email = ''; + $password = 'bad-password'; + + $mutation + = <<<MUTATION +mutation { + generateCustomerToken( + email: "{$email}" + password: "{$password}" + ) { + token + } +} +MUTATION; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('GraphQL response contains errors: Specify the "email" value.'); + $this->graphQlMutation($mutation); + } + + /** + * Verify customer with empty password + */ + public function testGenerateCustomerTokenWithEmptyPassword() + { + $email = 'customer@example.com'; + $password = ''; + + $mutation + = <<<MUTATION +mutation { + generateCustomerToken( + email: "{$email}" + password: "{$password}" + ) { + token + } +} +MUTATION; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('GraphQL response contains errors: Specify the "password" value.'); + $this->graphQlMutation($mutation); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php new file mode 100644 index 0000000000000..1cf33184714d9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\PageCache\UrlRewrite; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\UrlRewrite\Model\UrlFinderInterface; + +/** + * Test caching works for url resolver. + */ +class UrlResolverCacheTest extends GraphQlAbstract +{ + /** + * @inheritdoc + */ + protected function setUp() + { + $this->markTestSkipped( + 'This test will stay skipped until DEVOPS-4924 is resolved' + ); + } + + /** + * Tests that X-Magento-tags and cache debug headers are correct for product urlResolver + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testCacheTagsForProducts() + { + $productSku = 'p002'; + $urlKey = 'p002.html'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get($productSku, false, null, true); + $urlResolverQuery = $this->getUrlResolverQuery($urlKey); + $responseMiss = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); + $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); + $actualTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); + $expectedTags = ["cat_p", "cat_p_{$product->getId()}", "FPC"]; + $this->assertEquals($expectedTags, $actualTags); + + //cache-debug should be a MISS on first request + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + + //cache-debug should be a HIT on second request + $responseHit = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); + $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + //cached data should be correct + $this->assertNotEmpty($responseHit['body']); + $this->assertArrayNotHasKey('errors', $responseHit['body']); + $this->assertEquals('PRODUCT', $responseHit['body']['urlResolver']['type']); + } + /** + * Tests that X-Magento-tags and cache debug headers are correct for category urlResolver + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testCacheTagsForCategory() + { + $categoryUrlKey = 'cat-1.html'; + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = Bootstrap::getObjectManager()->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $categoryUrlKey, + 'store_id' => $storeId + ] + ); + $categoryId = $actualUrls->getEntityId(); + $query = $this->getUrlResolverQuery($categoryUrlKey); + $responseMiss = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); + $actualTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); + $expectedTags = ["cat_c", "cat_c_{$categoryId}", "FPC"]; + $this->assertEquals($expectedTags, $actualTags); + + //cache-debug should be a MISS on first request + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + + //cache-debug should be a HIT on second request + $responseHit = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); + $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + + //verify cached data is correct + $this->assertNotEmpty($responseHit['body']); + $this->assertArrayNotHasKey('errors', $responseHit['body']); + $this->assertEquals('CATEGORY', $responseHit['body']['urlResolver']['type']); + } + /** + * Test that X-Magento-Tags Cache debug headers are correct for cms page url resolver + * + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testUrlResolverCachingForCMSPage() + { + /** @var \Magento\Cms\Model\Page $page */ + $page = Bootstrap::getObjectManager()->get(\Magento\Cms\Model\Page::class); + $page->load('page100'); + $cmsPageId = $page->getId(); + $requestPath = $page->getIdentifier(); + + $query = $this->getUrlResolverQuery($requestPath); + $responseMiss = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); + $actualTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); + $expectedTags = ["cms_p", "cms_p_{$cmsPageId}", "FPC"]; + $this->assertEquals($expectedTags, $actualTags); + + //cache-debug should be a MISS on first request + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + + //cache-debug should be a HIT on second request + $responseHit = $this->graphQlQueryWithResponseHeaders($query); + $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + + //verify cached data is correct + $this->assertNotEmpty($responseHit['body']); + $this->assertArrayNotHasKey('errors', $responseHit['body']); + $this->assertEquals('CMS_PAGE', $responseHit['body']['urlResolver']['type']); + } + /** + * Tests that cache is invalidated when url key is updated and access the original request path + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testCacheIsInvalidatedForUrlResolver() + { + $productSku = 'p002'; + $urlKey = 'p002.html'; + $urlResolverQuery = $this->getUrlResolverQuery($urlKey); + $responseMiss = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); + //cache-debug should be a MISS on first request + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + + //cache-debug should be a HIT on second request + $urlResolverQuery = $this->getUrlResolverQuery($urlKey); + $responseHit = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); + $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get($productSku, false, null, true); + $product->setUrlKey('p002-new.html')->save(); + + //cache-debug should be a MISS after updating the url key and accessing the same requestPath or urlKey + $urlResolverQuery = $this->getUrlResolverQuery($urlKey); + $responseMiss = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + } + + /** + * Get url resolver query + * + * @param $urlKey + * @return string + */ + private function getUrlResolverQuery(string $urlKey): string + { + $query = <<<QUERY +{ + urlResolver(url:"{$urlKey}") + { + id + relative_url + canonical_url + type + } +} +QUERY; + return $query; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/CustomOptions.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/CustomOptions.php index 74452100b6955..0b18c3989e96b 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/CustomOptions.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/Product/CustomOptions.php @@ -45,7 +45,7 @@ public function __construct( if (isset($data['dataset']) && isset($this->params['repository'])) { $this->data = $repositoryFactory->get($this->params['repository'])->get($data['dataset']); - $this->data = $this->replaceData($this->data, mt_rand()); + $this->data = $this->replaceData($this->data, random_int(0, PHP_INT_MAX)); $this->customOptions = $this->data; } if (isset($data['import_products'])) { diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php index 1013404f42df1..acaed3d7ded36 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php @@ -102,7 +102,7 @@ class Shipping extends Form * * @var string */ - private $emailError = '#checkout-customer-email-error'; + private $emailError = '#ustomer-email-error'; /** * Get email error. diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml index 0973b968cba95..71115e402880c 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml @@ -8,7 +8,7 @@ <mapping strict="0"> <fields> <email> - <selector>#checkout-customer-email</selector> + <selector>#customer-email</selector> </email> <firstname /> <lastname /> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCancelSuccessMessageInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCancelSuccessMessageInShoppingCart.php index 1333a07b3e1fc..ae6e476203e4a 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCancelSuccessMessageInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCancelSuccessMessageInShoppingCart.php @@ -8,6 +8,7 @@ use Magento\Checkout\Test\Page\CheckoutCart; use Magento\Mtf\Constraint\AbstractConstraint; +use Magento\Mtf\Client\BrowserInterface; /** * Assert that success message about canceled order is present and correct. @@ -23,10 +24,18 @@ class AssertCancelSuccessMessageInShoppingCart extends AbstractConstraint * Assert that success message about canceled order is present and correct. * * @param CheckoutCart $checkoutCart + * @param BrowserInterface $browser * @return void */ - public function processAssert(CheckoutCart $checkoutCart) + public function processAssert(CheckoutCart $checkoutCart, BrowserInterface $browser) { + $path = $checkoutCart::MCA; + $browser->waitUntil( + function () use ($browser, $path) { + return $_ENV['app_frontend_url'] . $path . '/' === $browser->getUrl() . 'index/' ? true : null; + } + ); + $actualMessage = $checkoutCart->getMessagesBlock()->getSuccessMessage(); \PHPUnit\Framework\Assert::assertEquals( self::SUCCESS_MESSAGE, diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct/ConfigurableAttributesData.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct/ConfigurableAttributesData.php index ef86367a8079b..fdcf2d15d4288 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct/ConfigurableAttributesData.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Fixture/ConfigurableProduct/ConfigurableAttributesData.php @@ -360,7 +360,7 @@ protected function addVariationMatrix(array $variationsMatrix, array $attribute, /* If empty matrix add one empty row */ if (empty($variationsMatrix)) { - $variationIsolation = mt_rand(10000, 70000); + $variationIsolation = random_int(10000, 70000); $variationsMatrix = [ [ 'name' => "In configurable product {$variationIsolation}", @@ -370,7 +370,7 @@ protected function addVariationMatrix(array $variationsMatrix, array $attribute, } foreach ($variationsMatrix as $rowKey => $row) { - $randIsolation = mt_rand(1, 100); + $randIsolation = random_int(1, 100); $rowName = $row['name']; $rowSku = $row['sku']; $index = 1; diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutDeclinedTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutDeclinedTest.xml index d4f75f483d725..c5ffa5bf0fccd 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutDeclinedTest.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/OnePageCheckoutDeclinedTest.xml @@ -18,7 +18,7 @@ <data name="payment/method" xsi:type="string">payflowpro</data> <data name="creditCard/dataset" xsi:type="string">visa_default</data> <data name="configData" xsi:type="string">payflowpro, payflowpro_avs_street_does_not_match</data> - <data name="expectedErrorMessage" xsi:type="string">A server error stopped your order from being placed. Please try to place your order again.</data> + <data name="expectedErrorMessage" xsi:type="string">Transaction has been declined</data> <constraint name="Magento\Checkout\Test\Constraint\AssertCheckoutErrorMessage" /> </variation> <variation name="OnePageCheckoutDeclinedTestWithAVSZIP" summary="Place order via Payflow Pro with AVS ZIP verification fail" ticketId="MAGETWO-37483"> @@ -30,7 +30,7 @@ <data name="shipping/shipping_method" xsi:type="string">Fixed</data> <data name="payment/method" xsi:type="string">payflowpro</data> <data name="creditCard/dataset" xsi:type="string">visa_default</data> - <data name="expectedErrorMessage" xsi:type="string">A server error stopped your order from being placed. Please try to place your order again.</data> + <data name="expectedErrorMessage" xsi:type="string">Transaction has been declined</data> <data name="configData" xsi:type="string">payflowpro, payflowpro_use_avs_zip</data> <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S1</data> <constraint name="Magento\Checkout\Test\Constraint\AssertCheckoutErrorMessage" /> @@ -46,7 +46,7 @@ <data name="payment/method" xsi:type="string">payflowpro</data> <data name="creditCard/dataset" xsi:type="string">visa_cvv_mismatch</data> <data name="configData" xsi:type="string">payflowpro, payflowpro_avs_security_code_does_not_match</data> - <data name="expectedErrorMessage" xsi:type="string">A server error stopped your order from being placed. Please try to place your order again.</data> + <data name="expectedErrorMessage" xsi:type="string">Transaction has been declined</data> <constraint name="Magento\Checkout\Test\Constraint\AssertCheckoutErrorMessage" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SearchTermsReportEntityTest.php b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SearchTermsReportEntityTest.php index 108bbfcdf33ab..60be714e9dbbb 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SearchTermsReportEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/SearchTermsReportEntityTest.php @@ -96,7 +96,7 @@ public function test($product, $countProducts, $countSearch) */ protected function createProducts($product, $countProduct) { - $name = 'simpleProductName' . mt_rand(); + $name = 'simpleProductName' . random_int(0, PHP_INT_MAX); for ($i = 0; $i < $countProduct; $i++) { $productFixture = $this->fixtureFactory->createByCode( 'catalogProductSimple', diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Actions.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Actions.php index 7a6903ef47aac..332aafd6d898a 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Actions.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Actions.php @@ -107,7 +107,7 @@ class Actions extends Block * * @var string */ - protected $orderInvoiceCreditMemo = '#capture'; + protected $orderInvoiceCreditMemo = '#credit-memo'; /** * 'Refund' button. diff --git a/dev/tests/integration/testsuite/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsReadTest.php b/dev/tests/integration/testsuite/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsReadTest.php index 8195048b10af9..ab72a2e1b1dd2 100644 --- a/dev/tests/integration/testsuite/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsReadTest.php +++ b/dev/tests/integration/testsuite/Magento/AdminNotification/Controller/Adminhtml/Notification/MarkAsReadTest.php @@ -5,8 +5,16 @@ */ namespace Magento\AdminNotification\Controller\Adminhtml\Notification; +/** + * Testing markAsRead controller. + * + * @magentoAppArea adminhtml + */ class MarkAsReadTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * @inheritdoc + */ public function setUp() { $this->resource = 'Magento_AdminNotification::mark_as_read'; diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php index f1e7a10737604..5ca2bf1f73175 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php @@ -3,12 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Backend\Model\Auth; -use Magento\TestFramework\Bootstrap as TestHelper; -use Magento\TestFramework\Helper\Bootstrap; - /** * @magentoAppArea adminhtml * @magentoAppIsolation enabled @@ -22,15 +18,10 @@ class SessionTest extends \PHPUnit\Framework\TestCase private $auth; /** - * @var Session + * @var \Magento\Backend\Model\Auth\Session */ private $authSession; - /** - * @var SessionFactory - */ - private $authSessionFactory; - /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -39,12 +30,11 @@ class SessionTest extends \PHPUnit\Framework\TestCase protected function setUp() { parent::setUp(); - $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->objectManager->get(\Magento\Framework\Config\ScopeInterface::class) ->setCurrentScope(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); $this->auth = $this->objectManager->create(\Magento\Backend\Model\Auth::class); - $this->authSession = $this->objectManager->create(Session::class); - $this->authSessionFactory = $this->objectManager->get(SessionFactory::class); + $this->authSession = $this->objectManager->create(\Magento\Backend\Model\Auth\Session::class); $this->auth->setAuthStorage($this->authSession); $this->auth->logout(); } @@ -62,8 +52,8 @@ public function testIsLoggedIn($loggedIn) { if ($loggedIn) { $this->auth->login( - TestHelper::ADMIN_NAME, - TestHelper::ADMIN_PASSWORD + \Magento\TestFramework\Bootstrap::ADMIN_NAME, + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD ); } $this->assertEquals($loggedIn, $this->authSession->isLoggedIn()); @@ -73,23 +63,4 @@ public function loginDataProvider() { return [[false], [true]]; } - - /** - * Check that persisting user data is working. - */ - public function testStorage() - { - $this->auth->login(TestHelper::ADMIN_NAME, TestHelper::ADMIN_PASSWORD); - $user = $this->authSession->getUser(); - $acl = $this->authSession->getAcl(); - /** @var Session $session */ - $session = $this->authSessionFactory->create(); - $persistedUser = $session->getUser(); - $persistedAcl = $session->getAcl(); - - $this->assertEquals($user->getData(), $persistedUser->getData()); - $this->assertEquals($user->getAclRole(), $persistedUser->getAclRole()); - $this->assertEquals($acl->getRoles(), $persistedAcl->getRoles()); - $this->assertEquals($acl->getResources(), $persistedAcl->getResources()); - } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php index 88662a65c7428..d1252be2c4b53 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php @@ -3,12 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Backend\Model\Locale; use Magento\Framework\Locale\Resolver; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\User\Model\User; /** * @magentoAppArea adminhtml @@ -23,7 +20,7 @@ class ResolverTest extends \PHPUnit\Framework\TestCase protected function setUp() { parent::setUp(); - $this->_model = Bootstrap::getObjectManager()->create( + $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Backend\Model\Locale\Resolver::class ); } @@ -41,12 +38,12 @@ public function testSetLocaleWithDefaultLocale() */ public function testSetLocaleWithBaseInterfaceLocale() { - $user = Bootstrap::getObjectManager()->create(User::class); - $session = Bootstrap::getObjectManager()->get( + $user = new \Magento\Framework\DataObject(); + $session = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Auth\Session::class ); $session->setUser($user); - Bootstrap::getObjectManager()->get( + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Auth\Session::class )->getUser()->setInterfaceLocale( 'fr_FR' @@ -59,7 +56,7 @@ public function testSetLocaleWithBaseInterfaceLocale() */ public function testSetLocaleWithSessionLocale() { - Bootstrap::getObjectManager()->get( + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Session::class )->setSessionLocale( 'es_ES' @@ -72,7 +69,7 @@ public function testSetLocaleWithSessionLocale() */ public function testSetLocaleWithRequestLocale() { - $request = Bootstrap::getObjectManager() + $request = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Framework\App\RequestInterface::class); $request->setPostValue(['locale' => 'de_DE']); $this->_checkSetLocale('de_DE'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/DescriptionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/DescriptionTest.php new file mode 100644 index 0000000000000..e097109ff63bc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/DescriptionTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\View; + +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppArea frontend + */ +class DescriptionTest extends TestCase +{ + /** + * @var Description + */ + private $block; + + /** + * @var Registry + */ + private $registry; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->block = $objectManager->create(Description::class, [ + 'data' => [ + 'template' => 'Magento_Catalog::product/view/attribute.phtml' + ] + ]); + + $this->registry = $objectManager->get(Registry::class); + $this->registry->unregister('product'); + } + + public function testGetProductWhenNoProductIsRegistered() + { + $html = $this->block->toHtml(); + $this->assertEmpty($html); + } + + public function testGetProductWhenInvalidProductIsRegistered() + { + $this->registry->register('product', new \stdClass()); + $html = $this->block->toHtml(); + $this->assertEmpty($html); + } +} 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 1cb2caace4fa1..0c3e81fd52e81 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php @@ -23,17 +23,19 @@ class CompareTest extends \Magento\TestFramework\TestCase\AbstractController */ protected $productRepository; + /** + * @var \Magento\Framework\Data\Form\FormKey + */ + private $formKey; + /** * @inheritDoc */ protected function setUp() { parent::setUp(); - - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $this->productRepository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); + $this->formKey = $this->_objectManager->get(\Magento\Framework\Data\Form\FormKey::class); + $this->productRepository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); } /** @@ -44,16 +46,13 @@ protected function setUp() public function testAddAction() { $this->_requireVisitorWithNoProducts(); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @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', $product->getEntityId(), - $formKey->getFormKey() + $this->formKey->getFormKey() ) ); @@ -72,6 +71,31 @@ public function testAddAction() $this->_assertCompareListEquals([$product->getEntityId()]); } + /** + * Test adding disabled product to compare list. + * + * @return void + */ + public function testAddActionForDisabledProduct(): void + { + $this->_requireVisitorWithNoProducts(); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->setProductDisabled('simple_product_1'); + + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch( + sprintf( + 'catalog/product_compare/add/product/%s/form_key/%s?nocookie=1', + $product->getEntityId(), + $this->formKey->getFormKey() + ) + ); + + $this->assertRedirect(); + + $this->_assertCompareListEquals([]); + } + /** * Test comparing a product. * @@ -110,6 +134,24 @@ public function testRemoveAction() $this->_assertCompareListEquals([$restProduct->getEntityId()]); } + /** + * Test removing a disabled product from compare list. + * + * @return void + */ + public function testRemoveActionForDisabledProduct(): void + { + $this->_requireVisitorWithTwoProducts(); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->setProductDisabled('simple_product_1'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('catalog/product_compare/remove/product/' . $product->getEntityId()); + + $this->assertRedirect(); + $restProduct = $this->productRepository->get('simple_product_2'); + $this->_assertCompareListEquals([$product->getEntityId(), $restProduct->getEntityId()]); + } + /** * Test removing a product from compare list of a registered customer. * @@ -202,6 +244,21 @@ public function testRemoveActionProductNameXss() ); } + /** + * Set product status disabled. + * + * @param string $sku + * @return \Magento\Catalog\Api\Data\ProductInterface + */ + private function setProductDisabled(string $sku): \Magento\Catalog\Api\Data\ProductInterface + { + $product = $this->productRepository->get($sku); + $product->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) + ->save(); + + return $product; + } + /** * Preparing compare list. * @@ -214,6 +271,7 @@ protected function _prepareCompareListWithProductNameXss() $visitor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Customer\Model\Visitor::class); /** @var \Magento\Framework\Stdlib\DateTime $dateTime */ + // phpcs:ignore $visitor->setSessionId(md5(time()) . md5(microtime())) ->setLastVisitAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)) ->save(); @@ -241,6 +299,7 @@ protected function _requireVisitorWithNoProducts() $visitor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Customer\Model\Visitor::class); + // phpcs:ignore $visitor->setSessionId(md5(time()) . md5(microtime())) ->setLastVisitAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)) ->save(); @@ -265,6 +324,7 @@ protected function _requireVisitorWithTwoProducts() /** @var $visitor \Magento\Customer\Model\Visitor */ $visitor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Customer\Model\Visitor::class); + // phpcs:ignore $visitor->setSessionId(md5(time()) . md5(microtime())) ->setLastVisitAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)) ->save(); @@ -328,6 +388,7 @@ protected function _requireCustomerWithTwoProducts() /** @var $visitor \Magento\Customer\Model\Visitor */ $visitor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Customer\Model\Visitor::class); + // phpcs:ignore $visitor->setSessionId(md5(time()) . md5(microtime())) ->setLastVisitAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)) ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php new file mode 100644 index 0000000000000..f1e235f8c9bf2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Backend\Model\Auth; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Framework\Acl\Builder; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Bootstrap as TestBootstrap; + +/** + * Provide tests for CategoryRepository model. + */ +class CategoryRepositoryTest extends TestCase +{ + /** + * Test subject. + * + * @var CategoryRepositoryInterface + */ + private $repo; + + /** + * @var Auth + */ + private $auth; + + /** + * @var Builder + */ + private $aclBuilder; + + /** + * @var CategoryInterfaceFactory + */ + private $categoryFactory; + + /** + * Sets up common objects. + * + * @inheritDoc + */ + protected function setUp() + { + $this->repo = Bootstrap::getObjectManager()->create(CategoryRepositoryInterface::class); + $this->auth = Bootstrap::getObjectManager()->get(Auth::class); + $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); + $this->categoryFactory = Bootstrap::getObjectManager()->get(CategoryInterfaceFactory::class); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + parent::tearDown(); + + $this->auth->logout(); + $this->aclBuilder->resetRuntimeAcl(); + } + + /** + * Test authorization when saving category's design settings. + * + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testSaveDesign() + { + $category = $this->repo->get(333); + $this->auth->login(TestBootstrap::ADMIN_NAME, TestBootstrap::ADMIN_PASSWORD); + + //Admin doesn't have access to category's design. + $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_category_design'); + + $category->setCustomAttribute('custom_design', 2); + $category = $this->repo->save($category); + $customDesignAttribute = $category->getCustomAttribute('custom_design'); + $this->assertTrue(!$customDesignAttribute || !$customDesignAttribute->getValue()); + + //Admin has access to category' design. + $this->aclBuilder->getAcl() + ->allow(null, ['Magento_Catalog::categories', 'Magento_Catalog::edit_category_design']); + + $category->setCustomAttribute('custom_design', 2); + $category = $this->repo->save($category); + $this->assertNotEmpty($category->getCustomAttribute('custom_design')); + $this->assertEquals(2, $category->getCustomAttribute('custom_design')->getValue()); + + //Creating a new one + /** @var CategoryInterface $newCategory */ + $newCategory = $this->categoryFactory->create(); + $newCategory->setName('new category without design'); + $newCategory->setParentId($category->getParentId()); + $newCategory->setIsActive(true); + $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_category_design'); + $newCategory->setCustomAttribute('custom_design', 2); + $newCategory = $this->repo->save($newCategory); + $customDesignAttribute = $newCategory->getCustomAttribute('custom_design'); + $this->assertTrue(!$customDesignAttribute || !$customDesignAttribute->getValue()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php index cbde2fa1d2b20..fa2a0e5cb34b7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php @@ -7,9 +7,13 @@ namespace Magento\Catalog\Model; +use Magento\Backend\Model\Auth; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Bootstrap as TestBootstrap; +use Magento\Framework\Acl\Builder; /** * Provide tests for ProductRepository model. @@ -28,7 +32,7 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase private $productRepository; /** - * @var \Magento\Framework\Api\SearchCriteriaBuilder + * @var SearchCriteriaBuilder */ private $searchCriteriaBuilder; @@ -42,24 +46,38 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase */ private $productResource; + /* + * @var Auth + */ + private $auth; + + /** + * @var Builder + */ + private $aclBuilder; + /** * Sets up common objects */ protected function setUp() { - $this->productRepository = \Magento\Framework\App\ObjectManager::getInstance()->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class - ); - - $this->searchCriteriaBuilder = \Magento\Framework\App\ObjectManager::getInstance()->create( - \Magento\Framework\Api\SearchCriteriaBuilder::class - ); - + $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); + $this->searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); + $this->auth = Bootstrap::getObjectManager()->get(Auth::class); + $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); $this->productFactory = Bootstrap::getObjectManager()->get(ProductFactory::class); - $this->productResource = Bootstrap::getObjectManager()->get(ProductResource::class); + } - $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + /** + * @inheritDoc + */ + protected function tearDown() + { + parent::tearDown(); + + $this->auth->logout(); + $this->aclBuilder->resetRuntimeAcl(); } /** @@ -186,4 +204,32 @@ public function testUpdateProductSku() //clean up. $this->productRepository->delete($updatedProduct); } + + /** + * Test authorization when saving product's design settings. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoAppArea adminhtml + */ + public function testSaveDesign() + { + $product = $this->productRepository->get('simple'); + $this->auth->login(TestBootstrap::ADMIN_NAME, TestBootstrap::ADMIN_PASSWORD); + + //Admin doesn't have access to product's design. + $this->aclBuilder->getAcl()->deny(null, 'Magento_Catalog::edit_product_design'); + + $product->setCustomAttribute('custom_design', 2); + $product = $this->productRepository->save($product); + $this->assertEmpty($product->getCustomAttribute('custom_design')); + + //Admin has access to products' design. + $this->aclBuilder->getAcl() + ->allow(null, ['Magento_Catalog::products','Magento_Catalog::edit_product_design']); + + $product->setCustomAttribute('custom_design', 2); + $product = $this->productRepository->save($product); + $this->assertNotEmpty($product->getCustomAttribute('custom_design')); + $this->assertEquals(2, $product->getCustomAttribute('custom_design')->getValue()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/expected_categories.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/expected_categories.php index 744d368a467d5..beaa9f5dfb7f2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/expected_categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/_files/expected_categories.php @@ -10,17 +10,20 @@ 'value' => '2', 'is_active' => '1', 'label' => 'Default Category', + '__disableTmpl' => true, 'optgroup' => [ 0 => [ 'value' => '3', 'is_active' => '1', 'label' => 'Category 1', + '__disableTmpl' => true, 'optgroup' => [ 0 => [ 'value' => '4', 'is_active' => '1', 'label' => 'Category 1.1', + '__disableTmpl' => true, 'optgroup' => [ 0 => @@ -28,6 +31,7 @@ 'value' => '5', 'is_active' => '1', 'label' => 'Category 1.1.1', + '__disableTmpl' => true, ], ], ], @@ -35,6 +39,7 @@ 'value' => '13', 'is_active' => '1', 'label' => 'Category 1.2', + '__disableTmpl' => true, ], ], ], @@ -42,36 +47,43 @@ 'value' => '6', 'is_active' => '1', 'label' => 'Category 2', + '__disableTmpl' => true, ], 2 => [ 'value' => '7', 'is_active' => '1', 'label' => 'Movable', + '__disableTmpl' => true, ], 3 => [ 'value' => '8', 'is_active' => '0', 'label' => 'Inactive', + '__disableTmpl' => true, ], 4 => [ 'value' => '9', 'is_active' => '1', 'label' => 'Movable Position 1', + '__disableTmpl' => true, ], 5 => [ 'value' => '10', 'is_active' => '1', 'label' => 'Movable Position 2', + '__disableTmpl' => true, ], 6 => [ 'value' => '11', 'is_active' => '1', 'label' => 'Movable Position 3', + '__disableTmpl' => true, ], 7 => [ 'value' => '12', 'is_active' => '1', 'label' => 'Category 12', + '__disableTmpl' => true, ], ], ], diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php index 8b19c185b0d35..95424484380f1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php @@ -18,7 +18,14 @@ $mediaDirectory->create($targetDirPath); $mediaDirectory->create($targetTmpDirPath); -$targetTmpFilePath = $mediaDirectory->getAbsolutePath() . DIRECTORY_SEPARATOR . $targetTmpDirPath - . DIRECTORY_SEPARATOR . 'magento_image.jpg'; -copy(__DIR__ . '/magento_image.jpg', $targetTmpFilePath); -// Copying the image to target dir is not necessary because during product save, it will be moved there from tmp dir +$images = ['magento_image.jpg', 'magento_small_image.jpg', 'magento_thumbnail.jpg']; + +foreach ($images as $image) { + $targetTmpFilePath = $mediaDirectory->getAbsolutePath() . DIRECTORY_SEPARATOR . $targetTmpDirPath + . DIRECTORY_SEPARATOR . $image; + + $sourceFilePath = __DIR__ . DIRECTORY_SEPARATOR . $image; + + copy($sourceFilePath, $targetTmpFilePath); + // Copying the image to target dir is not necessary because during product save, it will be moved there from tmp dir +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_multiple_images.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_multiple_images.php new file mode 100644 index 0000000000000..44c425c50bc19 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_multiple_images.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require __DIR__ . '/product_image.php'; +require __DIR__ . '/product_simple.php'; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product->setStoreId(0) + ->setImage('/m/a/magento_image.jpg') + ->setSmallImage('/m/a/magento_image.jpg') + ->setThumbnail('/m/a/magento_thumbnail.jpg') + ->setSwatchImage('/m/a/magento_thumbnail.jpg') + ->setData('media_gallery', ['images' => [ + [ + 'file' => '/m/a/magento_image.jpg', + 'position' => 1, + 'label' => 'Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ], + [ + 'file' => '/m/a/magento_thumbnail.jpg', + 'position' => 2, + 'label' => 'Thumbnail Image', + 'disabled' => 0, + 'media_type' => 'image' + ], + ]]) + ->setCanSaveCustomOptions(true) + ->save(); diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/groups_array.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_multiple_images_rollback.php similarity index 51% rename from app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/groups_array.php rename to dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_multiple_images_rollback.php index dde65986e8a3e..c165ca930cdad 100644 --- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/_files/groups_array.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_multiple_images_rollback.php @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ -return ['some.key' => 'some.val', 'group.1' => ['fields' => ['g1.1' => ['value' => 'g1.1.val']]]]; +require __DIR__ . '/product_with_image_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 56c5db5572a31..137a3845b1efa 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -85,10 +85,10 @@ private function getExpectedIndexData() return [ 'configurable' => [ $skuId => 'configurable', - $configurableId => 'Option 1 | Option 2', - $nameId => 'Configurable Product | Configurable OptionOption 1 | Configurable OptionOption 2', - $taxClassId => 'Taxable Goods | Taxable Goods | Taxable Goods', - $statusId => 'Enabled | Enabled | Enabled' + $configurableId => 'Option 2', + $nameId => 'Configurable Product | Configurable OptionOption 2', + $taxClassId => 'Taxable Goods | Taxable Goods', + $statusId => 'Enabled | Enabled' ], 'index_enabled' => [ $skuId => 'index_enabled', diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php index af572c556bb07..4682453012952 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php @@ -13,10 +13,13 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote; use Magento\TestFramework\Helper\Bootstrap; /** * Class SessionTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SessionTest extends \PHPUnit\Framework\TestCase { @@ -86,6 +89,26 @@ public function testGetQuoteNotInitializedCustomerLoggedIn() $this->_validateCustomerDataInQuote($quote); } + /** + * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php + * @magentoAppIsolation enabled + */ + public function testGetQuoteWithMismatchingSession() + { + /** @var Quote $quote */ + $quote = Bootstrap::getObjectManager()->create(Quote::class); + /** @var \Magento\Quote\Model\ResourceModel\Quote $quoteResource */ + $quoteResource = Bootstrap::getObjectManager()->create(\Magento\Quote\Model\ResourceModel\Quote::class); + $quoteResource->load($quote, 'test01', 'reserved_order_id'); + + // Customer on quote is not logged in + $this->checkoutSession->setQuoteId($quote->getId()); + + $sessionQuote = $this->checkoutSession->getQuote(); + $this->assertEmpty($sessionQuote->getCustomerId()); + $this->assertNotEquals($quote->getId(), $sessionQuote->getId()); + } + /** * Tes merging of customer data into initialized quote object. * 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 60ccdb88676aa..5150f7a1eae18 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 @@ -27,6 +27,7 @@ class ResetQuoteAddressesTest extends \PHPUnit\Framework\TestCase */ public function testAfterRemoveItem(): void { + $this->login(1); /** @var Quote $quote */ $quote = Bootstrap::getObjectManager()->create(Quote::class); $quote->load('test_order_with_virtual_product', 'reserved_order_id'); @@ -75,4 +76,12 @@ public function testAfterRemoveItem(): void $this->assertEmpty($quoteBillingAddressUpdated->getPostcode()); $this->assertEmpty($quoteBillingAddressUpdated->getCity()); } + + private function login(int $customerId): void + { + /** @var \Magento\Customer\Model\Session $session */ + $session = Bootstrap::getObjectManager() + ->get(\Magento\Customer\Model\Session::class); + $session->loginById($customerId); + } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/FulltextGridSearchTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/FulltextGridSearchTest.php index c740609773b90..24ad1cd809ff2 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/FulltextGridSearchTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/FulltextGridSearchTest.php @@ -10,6 +10,9 @@ use Magento\TestFramework\TestCase\AbstractBackendController; /** + * Testing seach in grid. + * + * @magentoAppArea adminhtml * @magentoDataFixture Magento/Cms/Fixtures/page_list.php */ class FulltextGridSearchTest extends AbstractBackendController 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 c574869a83cab..af495841b9672 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 @@ -7,6 +7,7 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Response\HttpFactory as ResponseFactory; /** * Test for \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\DeleteFolder class. @@ -38,6 +39,11 @@ class DeleteFolderTest extends \PHPUnit\Framework\TestCase */ private $filesystem; + /** + * @var HttpFactory + */ + private $responseFactory; + /** * @inheritdoc */ @@ -49,6 +55,7 @@ protected function setUp() /** @var \Magento\Cms\Helper\Wysiwyg\Images $imagesHelper */ $this->imagesHelper = $objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class); $this->fullDirectoryPath = $this->imagesHelper->getStorageRoot(); + $this->responseFactory = $objectManager->get(ResponseFactory::class); $this->model = $objectManager->get(\Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\DeleteFolder::class); } @@ -83,6 +90,7 @@ public function testExecute() * can be removed. * * @magentoDataFixture Magento/Cms/_files/linked_media.php + * @magentoAppIsolation enabled */ public function testExecuteWithLinkedMedia() { @@ -106,6 +114,7 @@ public function testExecuteWithLinkedMedia() * under media directory. * * @return void + * @magentoAppIsolation enabled */ public function testExecuteWithWrongDirectoryName() { @@ -116,6 +125,31 @@ public function testExecuteWithWrongDirectoryName() $this->assertFileExists($this->fullDirectoryPath . $directoryName); } + /** + * Execute method to check that there is no ability to remove folder which is in excluded directories list. + * + * @return void + * @magentoAppIsolation enabled + */ + public function testExecuteWithExcludedDirectoryName() + { + $directoryName = 'downloadable'; + $expectedResponseMessage = 'We cannot delete directory /downloadable.'; + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $mediaDirectory->create($directoryName); + $this->assertFileExists($this->fullDirectoryPath . $directoryName); + + $this->model->getRequest()->setParams(['node' => $this->imagesHelper->idEncode($directoryName)]); + $this->model->getRequest()->setMethod('POST'); + $jsonResponse = $this->model->execute(); + $jsonResponse->renderResult($response = $this->responseFactory->create()); + $data = json_decode($response->getBody(), true); + + $this->assertTrue($data['error']); + $this->assertEquals($expectedResponseMessage, $data['message']); + $this->assertFileExists($this->fullDirectoryPath . $directoryName); + } + /** * @inheritdoc */ @@ -128,5 +162,8 @@ public static function tearDownAfterClass() if ($directory->isExist('wysiwyg')) { $directory->delete('wysiwyg'); } + if ($directory->isExist('downloadable')) { + $directory->delete('downloadable'); + } } } 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 00f56e5700415..9303a5eac7868 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 @@ -33,6 +33,11 @@ class UploadTest extends \PHPUnit\Framework\TestCase */ private $fullDirectoryPath; + /** + * @var string + */ + private $fullExcludedDirectoryPath; + /** * @var string */ @@ -60,11 +65,13 @@ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $directoryName = 'directory1'; + $excludedDirName = 'downloadable'; $this->filesystem = $this->objectManager->get(\Magento\Framework\Filesystem::class); /** @var \Magento\Cms\Helper\Wysiwyg\Images $imagesHelper */ $imagesHelper = $this->objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class); $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->fullDirectoryPath = $imagesHelper->getStorageRoot() . DIRECTORY_SEPARATOR . $directoryName; + $this->fullExcludedDirectoryPath = $imagesHelper->getStorageRoot() . DIRECTORY_SEPARATOR . $excludedDirName; $this->mediaDirectory->create($this->mediaDirectory->getRelativePath($this->fullDirectoryPath)); $this->responseFactory = $this->objectManager->get(ResponseFactory::class); $this->model = $this->objectManager->get(\Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\Upload::class); @@ -115,6 +122,34 @@ public function testExecute() $this->assertEquals($keys, $dataKeys); } + /** + * Execute method with excluded directory path and file name to check that file can't be uploaded. + * + * @return void + * @magentoAppIsolation enabled + */ + public function testExecuteWithExcludedDirectory() + { + $expectedError = 'We can\'t upload the file to current folder right now. Please try another folder.'; + $this->model->getRequest()->setParams(['type' => 'image/png']); + $this->model->getRequest()->setMethod('POST'); + $this->model->getStorage()->getSession()->setCurrentPath($this->fullExcludedDirectoryPath); + /** @var JsonResponse $jsonResponse */ + $jsonResponse = $this->model->execute(); + /** @var Response $response */ + $jsonResponse->renderResult($response = $this->responseFactory->create()); + $data = json_decode($response->getBody(), true); + + $this->assertEquals($expectedError, $data['error']); + $this->assertFalse( + $this->mediaDirectory->isExist( + $this->mediaDirectory->getRelativePath( + $this->fullExcludedDirectoryPath . DIRECTORY_SEPARATOR . $this->fileName + ) + ) + ); + } + /** * Execute method with correct directory path and file name to check that file can be uploaded to the directory * located under linked folder. diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php new file mode 100644 index 0000000000000..cd4674f95d722 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model; + +use Magento\Backend\Model\Auth; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Bootstrap as TestBootstrap; +use Magento\Framework\Acl\Builder; + +/** + * Test class for page repository. + */ +class PageRepositoryTest extends TestCase +{ + /** + * Test subject. + * + * @var PageRepositoryInterface + */ + private $repo; + + /** + * @var Auth + */ + private $auth; + + /** + * @var SearchCriteriaBuilder + */ + private $criteriaBuilder; + + /** + * @var Builder + */ + private $aclBuilder; + + /** + * Sets up common objects. + * + * @inheritDoc + */ + protected function setUp() + { + $this->repo = Bootstrap::getObjectManager()->create(PageRepositoryInterface::class); + $this->auth = Bootstrap::getObjectManager()->get(Auth::class); + $this->criteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); + $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + parent::tearDown(); + + $this->auth->logout(); + } + + /** + * Test authorization when saving page's design settings. + * + * @magentoDataFixture Magento/Cms/_files/pages.php + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testSaveDesign() + { + $pages = $this->repo->getList( + $this->criteriaBuilder->addFilter('identifier', 'page_design_blank')->create() + )->getItems(); + $page = array_pop($pages); + $this->auth->login(TestBootstrap::ADMIN_NAME, TestBootstrap::ADMIN_PASSWORD); + + //Admin doesn't have access to page's design. + $this->aclBuilder->getAcl()->deny(null, 'Magento_Cms::save_design'); + + $page->setCustomTheme('test'); + $page = $this->repo->save($page); + $this->assertNotEquals('test', $page->getCustomTheme()); + + //Admin has access to page' design. + $this->aclBuilder->getAcl()->allow(null, ['Magento_Cms::save', 'Magento_Cms::save_design']); + + $page->setCustomTheme('test'); + $page = $this->repo->save($page); + $this->assertEquals('test', $page->getCustomTheme()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php index e25934fb25ee1..5d256f2234a53 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php @@ -38,6 +38,7 @@ class StorageTest extends \PHPUnit\Framework\TestCase /** * @inheritdoc */ + // phpcs:disable public static function setUpBeforeClass() { self::$_baseDir = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( @@ -48,10 +49,12 @@ public static function setUpBeforeClass() } touch(self::$_baseDir . '/1.swf'); } + // phpcs:enable /** * @inheritdoc */ + // phpcs:ignore public static function tearDownAfterClass() { \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -104,6 +107,31 @@ public function testGetThumbsPath(): void ); } + /** + * @return void + */ + public function testDeleteDirectory(): void + { + $path = $this->objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class)->getCurrentPath(); + $dir = 'testDeleteDirectory'; + $fullPath = $path . $dir; + $this->storage->createDirectory($dir, $path); + $this->assertFileExists($fullPath); + $this->storage->deleteDirectory($fullPath); + $this->assertFileNotExists($fullPath); + } + + /** + * @return void + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage We cannot delete directory /downloadable. + */ + public function testDeleteDirectoryWithExcludedDirPath(): void + { + $dir = $this->objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class)->getCurrentPath() . 'downloadable'; + $this->storage->deleteDirectory($dir); + } + /** * @return void */ @@ -112,6 +140,7 @@ public function testUploadFile(): void $fileName = 'magento_small_image.jpg'; $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); $filePath = $tmpDirectory->getAbsolutePath($fileName); + // phpcs:disable $fixtureDir = realpath(__DIR__ . '/../../../../Catalog/_files'); copy($fixtureDir . DIRECTORY_SEPARATOR . $fileName, $filePath); @@ -125,31 +154,84 @@ public function testUploadFile(): void $this->storage->uploadFile(self::$_baseDir); $this->assertTrue(is_file(self::$_baseDir . DIRECTORY_SEPARATOR . $fileName)); + // phpcs:enable } /** + * @return void * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage File validation failed. + * @expectedExceptionMessage We can't upload the file to current folder right now. Please try another folder. + */ + public function testUploadFileWithExcludedDirPath(): void + { + $fileName = 'magento_small_image.jpg'; + $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); + $filePath = $tmpDirectory->getAbsolutePath($fileName); + // phpcs:disable + $fixtureDir = realpath(__DIR__ . '/../../../../Catalog/_files'); + copy($fixtureDir . DIRECTORY_SEPARATOR . $fileName, $filePath); + + $_FILES['image'] = [ + 'name' => $fileName, + 'type' => 'image/jpeg', + 'tmp_name' => $filePath, + 'error' => 0, + 'size' => 12500, + ]; + + $dir = $this->objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class)->getCurrentPath() . 'downloadable'; + $this->storage->uploadFile($dir); + // phpcs:enable + } + + /** + * @param string $fileName + * @param string $fileType + * @param string|null $storageType + * * @return void + * @dataProvider testUploadFileWithWrongExtensionDataProvider + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage File validation failed. */ - public function testUploadFileWithWrongExtension(): void + public function testUploadFileWithWrongExtension(string $fileName, string $fileType, ?string $storageType): void { - $fileName = 'text.txt'; $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); $filePath = $tmpDirectory->getAbsolutePath($fileName); - $file = fopen($filePath, "wb"); - fwrite($file, 'just a text'); + // phpcs:disable + $fixtureDir = realpath(__DIR__ . '/../../../_files'); + copy($fixtureDir . DIRECTORY_SEPARATOR . $fileName, $filePath); $_FILES['image'] = [ 'name' => $fileName, - 'type' => 'text/plain', + 'type' => $fileType, 'tmp_name' => $filePath, 'error' => 0, 'size' => 12500, ]; - $this->storage->uploadFile(self::$_baseDir); + $this->storage->uploadFile(self::$_baseDir, $storageType); $this->assertFalse(is_file(self::$_baseDir . DIRECTORY_SEPARATOR . $fileName)); + // phpcs:enable + } + + /** + * @return array + */ + public function testUploadFileWithWrongExtensionDataProvider(): array + { + return [ + [ + 'fileName' => 'text.txt', + 'fileType' => 'text/plain', + 'storageType' => null, + ], + [ + 'fileName' => 'test.swf', + 'fileType' => 'application/x-shockwave-flash', + 'storageType' => 'media', + ], + ]; } /** @@ -162,6 +244,7 @@ public function testUploadFileWithWrongFile(): void $fileName = 'file.gif'; $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); $filePath = $tmpDirectory->getAbsolutePath($fileName); + // phpcs:disable $file = fopen($filePath, "wb"); fwrite($file, 'just a text'); @@ -175,5 +258,6 @@ public function testUploadFileWithWrongFile(): void $this->storage->uploadFile(self::$_baseDir); $this->assertFalse(is_file(self::$_baseDir . DIRECTORY_SEPARATOR . $fileName)); + // phpcs:enable } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/test.swf b/dev/tests/integration/testsuite/Magento/Cms/_files/test.swf new file mode 100644 index 0000000000000..f0181cbb3e4cd Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/Cms/_files/test.swf differ diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/text.txt b/dev/tests/integration/testsuite/Magento/Cms/_files/text.txt new file mode 100644 index 0000000000000..9458610c5e7cf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/text.txt @@ -0,0 +1 @@ +just a text 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 24b68e804cd57..f5dbeb2ed12e4 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 @@ -6,6 +6,8 @@ namespace Magento\Config\Controller\Adminhtml\System; +use Magento\Config\Controller\Adminhtml\System\Config\Save; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\App\Request\Http as HttpRequest; @@ -67,6 +69,38 @@ public function testChangeBaseUrl() $this->resetBaseUrl($defaultHost); } + /** + * Test saving undeclared configs. + * + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testSavingUndeclared() + { + $request = $this->getRequest(); + $request->setPostValue([ + 'groups' => [ + 'non_existing' => [ + 'fields' => [ + 'non_existing_field' => [ + 'value' => 'some_value' + ] + ] + ] + ] + ]); + $request->setParam('section', 'web'); + $request->setMethod(HttpRequest::METHOD_POST); + /** @var Save $controller */ + $controller = Bootstrap::getObjectManager()->create(Save::class); + $controller->execute(); + + $this->assertSessionMessages($this->equalTo(['You saved the configuration.'])); + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = Bootstrap::getObjectManager()->get(ScopeConfigInterface::class); + $this->assertNull($scopeConfig->getValue('web/non_existing/non_existing_field')); + } + /** * Reset test framework default base url. * diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/SalesTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/SalesTest.php index c0772bc2be0e6..68410c44f29c3 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/SalesTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/SalesTest.php @@ -53,7 +53,7 @@ public function setUp() \Magento\Framework\View\LayoutInterface::class )->createBlock( \Magento\Customer\Block\Adminhtml\Edit\Tab\View\Sales::class, - 'sales_' . mt_rand(), + 'sales_' . random_int(0, PHP_INT_MAX), ['coreRegistry' => $this->coreRegistry] )->setTemplate( 'tab/view/sales.phtml' diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Config/Source/Group/MultiselectTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Config/Source/Group/MultiselectTest.php index a0b8c076d5059..9f121268135f8 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/Config/Source/Group/MultiselectTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Config/Source/Group/MultiselectTest.php @@ -33,10 +33,26 @@ public function testToOptionArray() $this->assertContains( $item, [ - ['value' => 1, 'label' => 'Default (General)'], - ['value' => 1, 'label' => 'General'], - ['value' => 2, 'label' => 'Wholesale'], - ['value' => 3, 'label' => 'Retailer'], + [ + 'value' => 1, + 'label' => 'Default (General)', + '__disableTmpl' => true, + ], + [ + 'value' => 1, + 'label' => 'General', + '__disableTmpl' => true, + ], + [ + 'value' => 2, + 'label' => 'Wholesale', + '__disableTmpl' => true, + ], + [ + 'value' => 3, + 'label' => 'Retailer', + '__disableTmpl' => true, + ], ] ); } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php index a5c69bcd3239e..a02f6f76e64d8 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php @@ -70,6 +70,9 @@ public function testGetCustomAttributesMetadata() ); } + /** + * @magentoAppIsolation enabled + */ public function testGetNestedOptionsCustomerAttributesMetadata() { $nestedOptionsAttribute = 'store_id'; diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php index dd55dcc8b47c7..7e16115ed2fef 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php @@ -267,7 +267,7 @@ public function cssDirectiveDataProvider() 'Empty or missing file' => [ TemplateTypesInterface::TYPE_HTML, 'file="css/non-existent-file.css"', - '/* Contents of css/non-existent-file.css could not be loaded or is empty */' + '/* Contents of the specified CSS file could not be loaded or is empty */' ], 'File with compilation error results in error message' => [ TemplateTypesInterface::TYPE_HTML, diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/query_array_output.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/query_array_output.php index c37632fe3e218..ae3735c251517 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/query_array_output.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/query_array_output.php @@ -34,7 +34,6 @@ 'resolver' => Magento\EavGraphQl\Model\Resolver\CustomAttributeMetadata::class, 'description' => 'Returns the attribute type, given an attribute code and entity type', 'cache' => [ - 'cacheTag' => 'cat_test', 'cacheIdentity' => Magento\EavGraphQl\Model\Resolver\CustomAttributeMetadata::class ] diff --git a/dev/tests/integration/testsuite/Magento/Framework/Model/ResourceModel/Db/ProfilerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Model/ResourceModel/Db/ProfilerTest.php index 7741f2a31fd90..99305ad2d4e80 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Model/ResourceModel/Db/ProfilerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Model/ResourceModel/Db/ProfilerTest.php @@ -9,6 +9,9 @@ use Magento\Framework\Config\ConfigOptionsListConstants; +/** + * Class ProfilerTest + */ class ProfilerTest extends \PHPUnit\Framework\TestCase { /** @@ -21,17 +24,27 @@ class ProfilerTest extends \PHPUnit\Framework\TestCase */ protected static $_testResourceName = 'testtest_0000_setup'; + /** + * @inheritdoc + * + * phpcs:disable Magento2.Functions.StaticFunction + */ public static function setUpBeforeClass() { - self::$_testResourceName = 'testtest_' . mt_rand(1000, 9999) . '_setup'; + self::$_testResourceName = 'testtest_' . random_int(1000, 9999) . '_setup'; \Magento\Framework\Profiler::enable(); - } + } // phpcs:enable + /** + * @inheritdoc + * + * phpcs:disable Magento2.Functions.StaticFunction + */ public static function tearDownAfterClass() { \Magento\Framework\Profiler::disable(); - } + } // phpcs:enable protected function setUp() { @@ -126,19 +139,20 @@ public function testProfilerDuringSqlException() $connection = $this->_getConnection(); try { - $connection->query('SELECT * FROM unknown_table'); + $connection->select()->from('unknown_table')->query()->fetch(); } catch (\Zend_Db_Statement_Exception $exception) { + $this->assertNotEmpty($exception); } if (!isset($exception)) { - $this->fail("Expected exception didn't thrown!"); + $this->fail("Expected exception wasn't thrown!"); } /** @var \Magento\Framework\App\ResourceConnection $resource */ $resource = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Framework\App\ResourceConnection::class); $testTableName = $resource->getTableName('setup_module'); - $connection->query('SELECT * FROM ' . $testTableName); + $connection->select()->from($testTableName)->query()->fetch(); /** @var \Magento\Framework\Model\ResourceModel\Db\Profiler $profiler */ $profiler = $connection->getProfiler(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php index c047bd4fffd3d..700c45ffc1119 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php @@ -95,7 +95,7 @@ public function testGetVersion() $model->create(); $this->assertEquals(0, $model->getVersion()); $changelogName = $this->resource->getTableName($model->getName()); - $this->connection->insert($changelogName, [$model->getColumnName() => mt_rand(1, 200)]); + $this->connection->insert($changelogName, [$model->getColumnName() => random_int(1, 200)]); $this->assertEquals($this->connection->lastInsertId($changelogName, 'version_id'), $model->getVersion()); $model->drop(); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php index 0aaa3f4e15bda..8586f47a0f7fa 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php @@ -35,7 +35,6 @@ 'match_query' => [ 'value' => '$match_term_override$', 'name' => 'match_query', - 'boost' => '1', 'match' => [ 0 => [ 'field' => 'match_field', @@ -51,7 +50,6 @@ ], 'must_query' => [ 'name' => 'must_query', - 'boost' => '1', 'filterReference' => [ 0 => [ 'clause' => 'must', @@ -62,7 +60,6 @@ ], 'should_query' => [ 'name' => 'should_query', - 'boost' => '1', 'filterReference' => [ 0 => [ 'clause' => 'should', @@ -73,7 +70,6 @@ ], 'not_query' => [ 'name' => 'not_query', - 'boost' => '1', 'filterReference' => [ 0 => [ 'clause' => 'not', @@ -84,7 +80,6 @@ ], 'match_query_2' => [ 'value' => '$match_term_override$', - 'boost' => '1', 'name' => 'match_query_2', 'match' => [ 0 => [ @@ -168,7 +163,6 @@ 'queries' => [ 'filter_query' => [ 'name' => 'filter_query', - 'boost' => '1', 'filterReference' => [ 0 => [ @@ -236,7 +230,6 @@ 'new_match_query' => [ 'value' => '$match_term$', 'name' => 'new_match_query', - 'boost' => '1', 'match' => [ 0 => [ diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Layout/MergeTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Layout/MergeTest.php index 8404884a7cf5c..c8c80a9647020 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/Layout/MergeTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Layout/MergeTest.php @@ -41,6 +41,11 @@ class MergeTest extends \PHPUnit\Framework\TestCase */ protected $_cache; + /** + * @var \Magento\Framework\Serialize\SerializerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $_serializer; + /** * @var \PHPUnit_Framework_MockObject_MockObject */ @@ -74,7 +79,8 @@ class MergeTest extends \PHPUnit\Framework\TestCase protected function setUp() { $files = []; - foreach (glob(__DIR__ . '/_mergeFiles/layout/*.xml') as $filename) { + $fileDriver = new \Magento\Framework\Filesystem\Driver\File(); + foreach ($fileDriver->readDirectory(__DIR__ . '/_mergeFiles/layout/') as $filename) { $files[] = new \Magento\Framework\View\File($filename, 'Magento_Widget'); } $fileSource = $this->getMockForAbstractClass(\Magento\Framework\View\File\CollectorInterface::class); @@ -100,6 +106,8 @@ protected function setUp() $this->_cache = $this->getMockForAbstractClass(\Magento\Framework\Cache\FrontendInterface::class); + $this->_serializer = $this->getMockForAbstractClass(\Magento\Framework\Serialize\SerializerInterface::class); + $this->_theme = $this->createMock(\Magento\Theme\Model\Theme::class); $this->_theme->expects($this->any())->method('isPhysical')->will($this->returnValue(true)); $this->_theme->expects($this->any())->method('getArea')->will($this->returnValue('area')); @@ -140,6 +148,7 @@ function ($filename) use ($fileDriver) { 'resource' => $this->_resource, 'appState' => $this->_appState, 'cache' => $this->_cache, + 'serializer' => $this->_serializer, 'theme' => $this->_theme, 'validator' => $this->_layoutValidator, 'logger' => $this->_logger, @@ -276,9 +285,16 @@ public function testLoadFileSystemWithPageLayout() public function testLoadCache() { + $cacheValue = [ + "pageLayout" => "1column", + "layout" => self::FIXTURE_LAYOUT_XML + ]; + $this->_cache->expects($this->at(0))->method('load') - ->with('LAYOUT_area_STORE20_100c6a4ccd050e33acef0553f24ef399961') - ->will($this->returnValue(self::FIXTURE_LAYOUT_XML)); + ->with('LAYOUT_area_STORE20_100c6a4ccd050e33acef0553f24ef399961_page_layout_merged') + ->will($this->returnValue(json_encode($cacheValue))); + + $this->_serializer->expects($this->once())->method('unserialize')->willReturn($cacheValue); $this->assertEmpty($this->_model->getHandles()); $this->assertEmpty($this->_model->asString()); @@ -424,8 +440,10 @@ public function testLoadWithInvalidLayout() ->method('isValid') ->willThrowException(new \Exception('Layout is invalid.')); + // phpcs:ignore Magento2.Security.InsecureFunction $suffix = md5(implode('|', $this->_model->getHandles())); - $cacheId = "LAYOUT_{$this->_theme->getArea()}_STORE{$this->scope->getId()}_{$this->_theme->getId()}{$suffix}"; + $cacheId = "LAYOUT_{$this->_theme->getArea()}_STORE{$this->scope->getId()}" + . "_{$this->_theme->getId()}{$suffix}_page_layout_merged"; $messages = $this->_layoutValidator->getMessages(); // Testing error message is logged with logger diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php index 4cc46a8e745e8..f25144c308c68 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php @@ -27,16 +27,34 @@ abstract class AbstractGraphqlCacheTest extends TestCase protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); - $this->usePageCachePlugin(); } /** - * Enable full page cache plugin + * Prepare a query and return a request to be used in the same test end to end + * + * @param string $query + * @return \Magento\Framework\App\Request\Http */ - protected function usePageCachePlugin(): void + protected function prepareRequest(string $query) : \Magento\Framework\App\Request\Http { - /** @var $registry \Magento\Framework\Registry */ - $registry = $this->objectManager->get(\Magento\Framework\Registry::class); - $registry->register('use_page_cache_plugin', true, true); + $cacheableQuery = $this->objectManager->get(\Magento\GraphQlCache\Model\CacheableQuery::class); + $cacheableQueryReflection = new \ReflectionProperty( + $cacheableQuery, + 'cacheTags' + ); + $cacheableQueryReflection->setAccessible(true); + $cacheableQueryReflection->setValue($cacheableQuery, []); + + /** @var \Magento\Framework\UrlInterface $urlInterface */ + $urlInterface = $this->objectManager->create(\Magento\Framework\UrlInterface::class); + //set unique URL + $urlInterface->setQueryParam('query', $query); + + $request = $this->objectManager->get(\Magento\Framework\App\Request\Http::class); + $request->setUri($urlInterface->getUrl('graphql')); + $request->setMethod('GET'); + //set the actual GET query + $request->setQueryValue('query', $query); + return $request; } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php index 62cda28a4493a..fd97399992c1c 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php @@ -91,6 +91,13 @@ public function testToCheckRequestCacheTagsForCategoryWithProducts(): void 'operationName' => 'GetCategoryWithProducts' ]; + /** @var \Magento\Framework\UrlInterface $urlInterface */ + $urlInterface = $this->objectManager->create(\Magento\Framework\UrlInterface::class); + //set unique URL + $urlInterface->setQueryParam('query', $queryParams['query']); + $urlInterface->setQueryParam('variables', $queryParams['variables']); + $urlInterface->setQueryParam('operationName', $queryParams['operationName']); + $this->request->setUri($urlInterface->getUrl('graphql')); $this->request->setPathInfo('/graphql'); $this->request->setMethod('GET'); $this->request->setParams($queryParams); diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php index 96f6685233f2c..be920fb200ff3 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php @@ -25,11 +25,6 @@ class CategoryCacheTest extends AbstractGraphqlCacheTest */ private $graphqlController; - /** - * @var Http - */ - private $request; - /** * @inheritdoc */ @@ -37,7 +32,6 @@ protected function setUp(): void { parent::setUp(); $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - $this->request = $this->objectManager->create(Http::class); } /** * Test cache tags and debug header for category and querying only for category @@ -59,10 +53,8 @@ public function testToCheckRequestCacheTagsForForCategory(): void } } QUERY; - $this->request->setPathInfo('/graphql'); - $this->request->setMethod('GET'); - $this->request->setQueryValue('query', $query); - $response = $this->graphqlController->dispatch($this->request); + $request = $this->prepareRequest($query); + $response = $this->graphqlController->dispatch($request); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); $expectedCacheTags = ['cat_c','cat_c_' . $categoryId,'FPC']; diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php index 7f992a0843f7c..746b37a88770a 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php @@ -23,9 +23,6 @@ class DeepNestedCategoriesAndProductsTest extends AbstractGraphqlCacheTest /** @var \Magento\GraphQl\Controller\GraphQl */ private $graphql; - /** @var Http */ - private $request; - /** * @inheritdoc */ @@ -33,7 +30,6 @@ protected function setUp(): void { parent::setUp(); $this->graphql = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - $this->request = $this->objectManager->get(Http::class); } /** @@ -112,10 +108,8 @@ public function testDispatchForCacheHeadersOnDeepNestedQueries(): void $expectedCacheTags = array_merge($expectedCacheTags, ['cat_c_'.$uniqueCategoryId]); } - $this->request->setPathInfo('/graphql'); - $this->request->setMethod('GET'); - $this->request->setQueryValue('query', $query); - $response = $this->graphql->dispatch($this->request); + $request = $this->prepareRequest($query); + $response = $this->graphql->dispatch($request); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); $this->assertEmpty( diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php index 78534176a3525..335067f8408df 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php @@ -8,7 +8,6 @@ namespace Magento\GraphQlCache\Controller\Catalog; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\App\Request\Http; use Magento\GraphQl\Controller\GraphQl; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; @@ -26,11 +25,6 @@ class ProductsCacheTest extends AbstractGraphqlCacheTest */ private $graphqlController; - /** - * @var Http - */ - private $request; - /** * @inheritdoc */ @@ -38,7 +32,6 @@ protected function setUp(): void { parent::setUp(); $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - $this->request = $this->objectManager->create(Http::class); } /** @@ -51,7 +44,6 @@ public function testToCheckRequestCacheTagsForProducts(): void /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - /** @var ProductInterface $product */ $product = $productRepository->get('simple1'); $query @@ -71,10 +63,8 @@ public function testToCheckRequestCacheTagsForProducts(): void } QUERY; - $this->request->setPathInfo('/graphql'); - $this->request->setMethod('GET'); - $this->request->setQueryValue('query', $query); - $response = $this->graphqlController->dispatch($this->request); + $request = $this->prepareRequest($query); + $response = $this->graphqlController->dispatch($request); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); $expectedCacheTags = ['cat_p', 'cat_p_' . $product->getId(), 'FPC']; @@ -103,10 +93,8 @@ public function testToCheckRequestNoTagsForProducts(): void } QUERY; - $this->request->setPathInfo('/graphql'); - $this->request->setMethod('GET'); - $this->request->setQueryValue('query', $query); - $response = $this->graphqlController->dispatch($this->request); + $request = $this->prepareRequest($query); + $response = $this->graphqlController->dispatch($request); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); $expectedCacheTags = ['FPC']; diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php index 160f5f9109f30..c9dca2a5a8372 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php @@ -8,7 +8,6 @@ namespace Magento\GraphQlCache\Controller\Cms; use Magento\Cms\Model\BlockRepository; -use Magento\Framework\App\Request\Http; use Magento\GraphQl\Controller\GraphQl; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; @@ -26,11 +25,6 @@ class BlockCacheTest extends AbstractGraphqlCacheTest */ private $graphqlController; - /** - * @var Http - */ - private $request; - /** * @inheritdoc */ @@ -38,24 +32,28 @@ protected function setUp(): void { parent::setUp(); $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - $this->request = $this->objectManager->create(Http::class); } /** * Test that the correct cache tags get added to request for cmsBlocks * * @magentoDataFixture Magento/Cms/_files/block.php + * @magentoDataFixture Magento/Cms/_files/blocks.php */ public function testCmsBlocksRequestHasCorrectTags(): void { - $blockIdentifier = 'fixture_block'; + /** @var BlockRepository $blockRepository */ $blockRepository = $this->objectManager->get(BlockRepository::class); - $block = $blockRepository->getById($blockIdentifier); - $query + $block1Identifier = 'fixture_block'; + $block1 = $blockRepository->getById($block1Identifier); + $block2Identifier = 'enabled_block'; + $block2 = $blockRepository->getById($block2Identifier); + + $queryBlock1 = <<<QUERY { - cmsBlocks(identifiers: ["$blockIdentifier"]) { + cmsBlocks(identifiers: ["$block1Identifier"]) { items { title identifier @@ -65,12 +63,72 @@ public function testCmsBlocksRequestHasCorrectTags(): void } QUERY; - $this->request->setPathInfo('/graphql'); - $this->request->setMethod('GET'); - $this->request->setQueryValue('query', $query); - $response = $this->graphqlController->dispatch($this->request); + $queryBlock2 + = <<<QUERY + { + cmsBlocks(identifiers: ["$block2Identifier"]) { + items { + title + identifier + content + } + } +} +QUERY; + + // check to see that the first entity gets a MISS when called the first time + $request = $this->prepareRequest($queryBlock1); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cms_b', 'cms_b_' . $block1->getId(), 'cms_b_' . $block1->getIdentifier(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // check to see that the second entity gets a miss when called the first time + $request = $this->prepareRequest($queryBlock2); + $response = $this->graphqlController->dispatch($request); $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $expectedCacheTags = ['cms_b', 'cms_b_' . $block->getId(), 'cms_b_' . $block->getIdentifier(), 'FPC']; + $expectedCacheTags = ['cms_b', 'cms_b_' . $block2->getId(), 'cms_b_' . $block2->getIdentifier(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // check to see that the first entity gets a HIT when called the second time + $request = $this->prepareRequest($queryBlock1); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cms_b', 'cms_b_' . $block1->getId(), 'cms_b_' . $block1->getIdentifier(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // check to see that the second entity gets a HIT when called the second time + $request = $this->prepareRequest($queryBlock2); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cms_b', 'cms_b_' . $block2->getId(), 'cms_b_' . $block2->getIdentifier(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $block1->setTitle('something else that causes invalidation'); + $blockRepository->save($block1); + + // check to see that the first entity gets a MISS and it was invalidated + $request = $this->prepareRequest($queryBlock1); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cms_b', 'cms_b_' . $block1->getId(), 'cms_b_' . $block1->getIdentifier(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // check to see that the first entity gets a HIT when called the second time + $request = $this->prepareRequest($queryBlock1); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cms_b', 'cms_b_' . $block1->getId(), 'cms_b_' . $block1->getIdentifier(), 'FPC']; $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); $actualCacheTags = explode(',', $rawActualCacheTags); $this->assertEquals($expectedCacheTags, $actualCacheTags); diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php index 8d4bbfc0f2b17..0248f870a5f11 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php @@ -8,7 +8,7 @@ namespace Magento\GraphQlCache\Controller\Cms; use Magento\Cms\Model\GetPageByIdentifier; -use Magento\Framework\App\Request\Http; +use Magento\Cms\Model\PageRepository; use Magento\GraphQl\Controller\GraphQl; use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; @@ -27,11 +27,6 @@ class CmsPageCacheTest extends AbstractGraphqlCacheTest */ private $graphqlController; - /** - * @var Http - */ - private $request; - /** * @inheritdoc */ @@ -39,7 +34,6 @@ protected function setUp(): void { parent::setUp(); $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); - $this->request = $this->objectManager->create(Http::class); } /** @@ -49,13 +43,106 @@ protected function setUp(): void */ public function testToCheckCmsPageRequestCacheTags(): void { - $cmsPage = $this->objectManager->get(GetPageByIdentifier::class)->execute('page100', 0); - $pageId = $cmsPage->getId(); + $cmsPage100 = $this->objectManager->get(GetPageByIdentifier::class)->execute('page100', 0); + $pageId100 = $cmsPage100->getId(); + + $cmsPageBlank = $this->objectManager->get(GetPageByIdentifier::class)->execute('page_design_blank', 0); + $pageIdBlank = $cmsPageBlank->getId(); + + $queryCmsPage100 = $this->getQuery($pageId100); + $queryCmsPageBlank = $this->getQuery($pageIdBlank); + + // check to see that the first entity gets a MISS when called the first time + $request = $this->prepareRequest($queryCmsPage100); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals( + 'MISS', + $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), + "expected MISS on page page100 id {$queryCmsPage100}" + ); + $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId100 , 'FPC']; + $this->assertEquals($expectedCacheTags, $requestedCacheTags); + + // check to see that the second entity gets a miss when called the first time + $request = $this->prepareRequest($queryCmsPageBlank); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals( + 'MISS', + $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), + "expected MISS on page pageBlank id {$pageIdBlank}" + ); + $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cms_p', 'cms_p_' .$pageIdBlank , 'FPC']; + $this->assertEquals($expectedCacheTags, $requestedCacheTags); + + // check to see that the first entity gets a HIT when called the second time + $request = $this->prepareRequest($queryCmsPage100); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals( + 'HIT', + $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), + "expected HIT on page page100 id {$queryCmsPage100}" + ); + $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId100 , 'FPC']; + $this->assertEquals($expectedCacheTags, $requestedCacheTags); + + // check to see that the second entity gets a HIT when called the second time + $request = $this->prepareRequest($queryCmsPageBlank); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals( + 'HIT', + $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), + "expected HIT on page pageBlank id {$pageIdBlank}" + ); + $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cms_p', 'cms_p_' .$pageIdBlank , 'FPC']; + $this->assertEquals($expectedCacheTags, $requestedCacheTags); - $query = - <<<QUERY + /** @var PageRepository $pageRepository */ + $pageRepository = $this->objectManager->get(PageRepository::class); + + $page = $pageRepository->getById($pageId100); + $page->setTitle('something else that causes invalidation'); + $pageRepository->save($page); + + // check to see that the first entity gets a MISS and it was invalidated + $request = $this->prepareRequest($queryCmsPage100); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals( + 'MISS', + $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), + "expected MISS on page page100 id {$queryCmsPage100}" + ); + $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId100 , 'FPC']; + $this->assertEquals($expectedCacheTags, $requestedCacheTags); + + // check to see that the first entity gets a HIT when called the second time + $request = $this->prepareRequest($queryCmsPage100); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals( + 'HIT', + $response->getHeader('X-Magento-Cache-Debug')->getFieldValue(), + "expected MISS on page page100 id {$queryCmsPage100}" + ); + $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId100 , 'FPC']; + $this->assertEquals($expectedCacheTags, $requestedCacheTags); + } + + /** + * Get cms query + * + * @param string $id + * @return string + */ + private function getQuery(string $id) : string + { + $queryCmsPage = <<<QUERY { - cmsPage(id: $pageId) { + cmsPage(id: $id) { url_key title content @@ -67,14 +154,6 @@ public function testToCheckCmsPageRequestCacheTags(): void } } QUERY; - - $this->request->setPathInfo('/graphql'); - $this->request->setMethod('GET'); - $this->request->setQueryValue('query', $query); - $response = $this->graphqlController->dispatch($this->request); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId , 'FPC']; - $this->assertEquals($expectedCacheTags, $requestedCacheTags); + return $queryCmsPage; } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/UrlRewrite/AllEntitiesUrlResolverCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/UrlRewrite/AllEntitiesUrlResolverCacheTest.php new file mode 100644 index 0000000000000..7accb1d7d0b26 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/UrlRewrite/AllEntitiesUrlResolverCacheTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\UrlRewrite; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\GraphQl\Controller\GraphQl; +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\GetPageByIdentifierInterface; + +/** + * Test caching works for categoryUrlResolver + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ +class AllEntitiesUrlResolverCacheTest extends AbstractGraphqlCacheTest +{ + /** + * @var GraphQl + */ + private $graphqlController; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->graphqlController = $this->objectManager->get(GraphQl::class); + } + + /** + * Tests that X-Magento-tags and cache debug headers are correct for category urlResolver + * + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + * @magentoDataFixture Magento/Cms/_files/pages.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testAllEntitiesUrlResolverRequestHasCorrectTags() + { + $categoryUrlKey = 'cat-1.html'; + $productUrlKey = 'p002.html'; + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $categoryUrlKey, + 'store_id' => $storeId + ] + ); + $categoryId = $actualUrls->getEntityId(); + $categoryQuery = $this->getQuery($categoryUrlKey); + + $productQuery = $this->getQuery($productUrlKey); + + /** @var GetPageByIdentifierInterface $page */ + $page = $this->objectManager->get(GetPageByIdentifierInterface::class); + /** @var PageInterface $cmsPage */ + $cmsPage = $page->execute('page100', 0); + $cmsPageId = $cmsPage->getId(); + $requestPath = $cmsPage->getIdentifier(); + $pageQuery = $this->getQuery($requestPath); + + // query category for MISS + $request = $this->prepareRequest($categoryQuery); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // query product for MISS + $request = $this->prepareRequest($productQuery); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cat_p', 'cat_p_' . $product->getId(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // query page for MISS + $request = $this->prepareRequest($pageQuery); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cms_p','cms_p_' . $cmsPageId,'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // query category for HIT + $request = $this->prepareRequest($categoryQuery); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // query product for HIT + $request = $this->prepareRequest($productQuery); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cat_p', 'cat_p_' . $product->getId(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // query product for HIT + $request = $this->prepareRequest($pageQuery); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('HIT', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cms_p','cms_p_' . $cmsPageId,'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $product->setUrlKey('something-else-that-invalidates-the-cache'); + $productRepository->save($product); + $productQuery = $this->getQuery('something-else-that-invalidates-the-cache.html'); + + // query category for MISS + $request = $this->prepareRequest($categoryQuery); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // query product for HIT + $request = $this->prepareRequest($productQuery); + $response = $this->graphqlController->dispatch($request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cat_p', 'cat_p_' . $product->getId(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } + + /** + * Get urlResolver query + * + * @param string $id + * @return string + */ + private function getQuery(string $requestPath) : string + { + $resolverQuery = <<<QUERY +{ + urlResolver(url:"{$requestPath}") + { + id + relative_url + canonical_url + type + } +} +QUERY; + return $resolverQuery; + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DeleteTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DeleteTest.php new file mode 100644 index 0000000000000..91764684da173 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DeleteTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Controller\Adminhtml\Export\File; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for \Magento\ImportExport\Controller\Adminhtml\Export\File\Delete class. + */ +class DeleteTest extends AbstractBackendController +{ + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @var string + */ + private $fileName = 'catalog_product.csv'; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $filesystem = $this->_objectManager->get(Filesystem::class); + $sourceFilePath = __DIR__ . '/../../Import/_files' . DIRECTORY_SEPARATOR . $this->fileName; + $destinationFilePath = 'export' . DIRECTORY_SEPARATOR . $this->fileName; + //Refers to tests 'var' directory + $this->varDirectory = $filesystem->getDirectoryRead(DirectoryList::VAR_DIR); + //Refers to application root directory + $rootDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $rootDirectory->copyFile($sourceFilePath, $this->varDirectory->getAbsolutePath($destinationFilePath)); + } + + /** + * Check that file can be removed under var/export directory. + * + * @return void + * @magentoConfigFixture default_store admin/security/use_form_key 1 + */ + public function testExecute(): void + { + $request = $this->getRequest(); + $request->setParam('filename', $this->fileName); + $request->setMethod(Http::METHOD_POST); + + if ($this->varDirectory->isExist('export/' . $this->fileName)) { + $this->dispatch('backend/admin/export_file/delete'); + } else { + throw new \AssertionError('Export product file supposed to exist'); + } + + $this->assertFalse($this->varDirectory->isExist('export/' . $this->fileName)); + } + + /** + * @inheritdoc + */ + public static function tearDownAfterClass() + { + $filesystem = Bootstrap::getObjectManager()->get(Filesystem::class); + /** @var WriteInterface $directory */ + $directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + if ($directory->isExist('export')) { + $directory->delete('export'); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php new file mode 100644 index 0000000000000..073ecc6fd06a4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Controller\Adminhtml\Export\File; + +use Magento\Backend\Model\Auth\Session; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Backend\Model\UrlInterface as BackendUrl; +use Magento\Backend\Model\Auth; +use Magento\TestFramework\Bootstrap as TestBootstrap; + +/** + * Test for \Magento\ImportExport\Controller\Adminhtml\Export\File\Download class. + */ +class DownloadTest extends AbstractBackendController +{ + /** + * @var string + */ + private $fileName = 'catalog_product.csv'; + + /** + * @var string + */ + private $filesize; + + /** + * @var Auth + */ + private $auth; + + /** + * @var BackendUrl + */ + private $backendUrl; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $filesystem = $this->_objectManager->get(Filesystem::class); + $auth = $this->_objectManager->get(Auth::class); + $auth->getAuthStorage()->setIsFirstPageAfterLogin(false); + $this->backendUrl = $this->_objectManager->get(BackendUrl::class); + $this->backendUrl->turnOnSecretKey(); + + $sourceFilePath = __DIR__ . '/../../Import/_files' . DIRECTORY_SEPARATOR . $this->fileName; + $destinationFilePath = 'export' . DIRECTORY_SEPARATOR . $this->fileName; + //Refers to tests 'var' directory + $varDirectory = $filesystem->getDirectoryRead(DirectoryList::VAR_DIR); + //Refers to application root directory + $rootDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $rootDirectory->copyFile($sourceFilePath, $varDirectory->getAbsolutePath($destinationFilePath)); + $this->filesize = $varDirectory->stat($destinationFilePath)['size']; + } + + /** + * Check that file can be downloaded. + * + * @return void + * @magentoConfigFixture default_store admin/security/use_form_key 1 + * @magentoAppArea adminhtml + */ + public function testExecute(): void + { + $request = $this->getRequest(); + list($routeName, $controllerName, $actionName) = explode('/', Download::URL); + $request->setMethod(Http::METHOD_GET) + ->setRouteName($routeName) + ->setControllerName($controllerName) + ->setActionName($actionName); + $request->setParam('filename', $this->fileName); + $request->setParam(BackendUrl::SECRET_KEY_PARAM_NAME, $this->backendUrl->getSecretKey()); + + ob_start(); + $this->dispatch('backend/admin/export_file/download'); + ob_end_clean(); + + $contentType = $this->getResponse()->getHeader('content-type'); + $contentLength = $this->getResponse()->getHeader('content-length'); + $contentDisposition = $this->getResponse()->getHeader('content-disposition'); + + $this->assertEquals(200, $this->getResponse()->getStatusCode(), 'Incorrect response status code'); + $this->assertEquals( + 'application/octet-stream', + $contentType->getFieldValue(), + 'Incorrect response header "content-type"' + ); + $this->assertEquals( + 'attachment; filename="export/' . $this->fileName . '"', + $contentDisposition->getFieldValue(), + 'Incorrect response header "content-disposition"' + ); + $this->assertEquals( + $this->filesize, + $contentLength->getFieldValue(), + 'Incorrect response header "content-length"' + ); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->auth = null; + + parent::tearDown(); + } + + /** + * @inheritdoc + */ + public static function tearDownAfterClass() + { + $filesystem = Bootstrap::getObjectManager()->get(Filesystem::class); + /** @var WriteInterface $directory */ + $directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + if ($directory->isExist('export')) { + $directory->delete('export'); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php index 93fa04806d577..0c1f2d2fcc8d7 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php @@ -134,13 +134,24 @@ public function testValidateSourceException() $this->_model->validateSource($source); } - public function testGetEntity() + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Entity is unknown + */ + public function testGetUnknownEntity() { $entityName = 'entity_name'; $this->_model->setEntity($entityName); $this->assertSame($entityName, $this->_model->getEntity()); } + public function testGetEntity() + { + $entityName = 'catalog_product'; + $this->_model->setEntity($entityName); + $this->assertSame($entityName, $this->_model->getEntity()); + } + /** * @expectedException \Magento\Framework\Exception\LocalizedException * @expectedExceptionMessage Entity is unknown diff --git a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/System/Config/OauthSectionTest.php b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/System/Config/OauthSectionTest.php index ac5d8005180b4..b163830c72b18 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/System/Config/OauthSectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/System/Config/OauthSectionTest.php @@ -8,6 +8,11 @@ namespace Magento\Integration\Block\Adminhtml\System\Config; +/** + * Testing Oauth section in configs. + * + * @magentoAppArea adminhtml + */ class OauthSectionTest extends \Magento\TestFramework\TestCase\AbstractBackendController { /** diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Controller/Adminhtml/System/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Adminhtml/System/ConfigTest.php new file mode 100644 index 0000000000000..748998ef84c69 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Adminhtml/System/ConfigTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Controller\Adminhtml\System; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * @magentoAppArea adminhtml + */ +class ConfigTest extends \Magento\TestFramework\TestCase\AbstractBackendController +{ + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * + * @dataProvider saveMerchantCountryDataProvider + * + * @param string $section + * @param array $groups + * @return void + */ + public function testSaveMerchantCountry(string $section, array $groups): void + { + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = Bootstrap::getObjectManager()->get(ScopeConfigInterface::class); + + $request = $this->getRequest(); + $request->setPostValue($groups) + ->setParam('section', $section) + ->setMethod(HttpRequest::METHOD_POST); + + $this->dispatch('backend/admin/system_config/save'); + + $this->assertSessionMessages($this->equalTo(['You saved the configuration.'])); + + $this->assertEquals( + 'GB', + $scopeConfig->getValue('paypal/general/merchant_country') + ); + } + + /** + * @return array + */ + public function saveMerchantCountryDataProvider(): array + { + return [ + [ + 'section' => 'paypal', + 'groups' => [ + 'groups' => [ + 'general' => [ + 'fields' => [ + 'merchant_country' => ['value' => 'GB'], + ], + ], + ], + ], + ], + [ + 'section' => 'payment', + 'groups' => [ + 'groups' => [ + 'account' => [ + 'fields' => [ + 'merchant_country' => ['value' => 'GB'], + ], + ], + ], + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/FormTest.php index abdbab2c24d16..952116784640d 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/FormTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/FormTest.php @@ -68,7 +68,7 @@ protected function setUp() $layout = $this->objectManager->get(LayoutInterface::class); $this->block = $layout->createBlock( Form::class, - 'order_create_block' . mt_rand(), + 'order_create_block' . random_int(0, PHP_INT_MAX), ['sessionQuote' => $this->session] ); parent::setUp(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/View/InfoTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/View/InfoTest.php index b3fee1124d15f..cede79b1687a1 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/View/InfoTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/View/InfoTest.php @@ -21,7 +21,7 @@ public function testCustomerGridAction() /** @var \Magento\Sales\Block\Adminhtml\Order\View\Info $infoBlock */ $infoBlock = $layout->createBlock( \Magento\Sales\Block\Adminhtml\Order\View\Info::class, - 'info_block' . mt_rand(), + 'info_block' . random_int(0, PHP_INT_MAX), [] ); @@ -38,7 +38,7 @@ public function testGetCustomerGroupName() /** @var \Magento\Sales\Block\Adminhtml\Order\View\Info $customerGroupBlock */ $customerGroupBlock = $layout->createBlock( \Magento\Sales\Block\Adminhtml\Order\View\Info::class, - 'info_block' . mt_rand(), + 'info_block' . random_int(0, PHP_INT_MAX), ['registry' => $this->_putOrderIntoRegistry()] ); @@ -60,7 +60,7 @@ public function testGetCustomerAccountData() /** @var \Magento\Sales\Block\Adminhtml\Order\View\Info $customerGroupBlock */ $customerGroupBlock = $layout->createBlock( \Magento\Sales\Block\Adminhtml\Order\View\Info::class, - 'info_block' . mt_rand(), + 'info_block' . random_int(0, PHP_INT_MAX), ['registry' => $this->_putOrderIntoRegistry($orderData)] ); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php index 1067a474e19aa..80dfc17f522f1 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php @@ -40,6 +40,7 @@ public function testViewOrderAsGuest() public function testViewOrderAsLoggedIn() { $this->login(1); + $this->getRequest()->setMethod(Request::METHOD_POST); $this->dispatch('sales/guest/view/'); $this->assertRedirect($this->stringContains('sales/order/history/')); } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ViewTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ViewTest.php new file mode 100644 index 0000000000000..5a912c2960ab6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ViewTest.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Guest; + +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test for \Magento\Sales\Controller\Guest\View class. + */ +class ViewTest extends AbstractController +{ + /** + * Check that controller applied GET requests. + */ + public function testExecuteWithGetRequest() + { + $this->getRequest()->setMethod(Request::METHOD_GET); + $this->dispatch('sales/guest/view/'); + + $this->assertRedirect($this->stringContains('sales/guest/form')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Helper/AdminTest.php b/dev/tests/integration/testsuite/Magento/Sales/Helper/AdminTest.php new file mode 100644 index 0000000000000..a1f64559e9e82 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Helper/AdminTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Helper; + +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Tests \Magento\Sales\Helper\Admin + */ +class AdminTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Admin + */ + private $helper; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->helper = Bootstrap::getObjectManager()->create(Admin::class); + } + + /** + * @param string $data + * @param string $expected + * @param null|array $allowedTags + * @return void + * + * @dataProvider escapeHtmlWithLinksDataProvider + */ + public function testEscapeHtmlWithLinks(string $data, string $expected, $allowedTags = null): void + { + $actual = $this->helper->escapeHtmlWithLinks($data, $allowedTags); + $this->assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function escapeHtmlWithLinksDataProvider(): array + { + return [ + [ + '<a>some text in tags</a>', + '<a>some text in tags</a>', + 'allowedTags' => null, + ], + [ + // @codingStandardsIgnoreStart + 'Authorized amount of €30.00. Transaction ID: "<a target="_blank" href="https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=123456789QWERTY">123456789QWERTY</a>"', + 'Authorized amount of €30.00. Transaction ID: "<a href="https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=123456789QWERTY">123456789QWERTY</a>"', + // @codingStandardsIgnoreEnd + 'allowedTags' => ['b', 'br', 'strong', 'i', 'u', 'a'], + ], + [ + 'Transaction ID: "<a target="_blank" href="https://www.paypal.com/?id=XX123XX">XX123XX</a>"', + 'Transaction ID: "<a href="https://www.paypal.com/?id=XX123XX">XX123XX</a>"', + 'allowedTags' => ['b', 'br', 'strong', 'i', 'u', 'a'], + ], + [ + '<a>some text in tags</a>', + '<a>some text in tags</a>', + 'allowedTags' => ['a'], + ], + [ + "<a><script>alert(1)</script></a>", + '<a>alert(1)</a>', + 'allowedTags' => ['a'], + ], + [ + '<a href=\"#\">Foo</a>', + '<a href="#">Foo</a>', + 'allowedTags' => ['a'], + ], + [ + "<a href=http://example.com?foo=1&bar=2&baz[name]=BAZ>Foo</a>", + '<a href="http://example.com?foo=1&bar=2&baz%5Bname%5D=BAZ">Foo</a>', + 'allowedTags' => ['a'], + ], + [ + "<a href=\"javascript:alert(59)\">Foo</a>", + '<a href="#">Foo</a>', + 'allowedTags' => ['a'], + ], + [ + "<a href=\"http://example1.com\" href=\"http://example2.com\">Foo</a>", + '<a href="http://example1.com">Foo</a>', + 'allowedTags' => ['a'], + ], + [ + "<a href=\"http://example.com?foo=text with space\">Foo</a>", + '<a href="http://example.com?foo=text%20with%20space">Foo</a>', + 'allowedTags' => ['a'], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/CodeLimitManagerTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/CodeLimitManagerTest.php new file mode 100644 index 0000000000000..df32e4ffc057e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/CodeLimitManagerTest.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon; + +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; +use Magento\SalesRule\Api\Exception\CodeRequestLimitException; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Model\Session as CustomerSession; + +/** + * Test for captcha based implementation. + * + * @magentoAppArea frontend + */ +class CodeLimitManagerTest extends TestCase +{ + /** + * @var CodeLimitManager + */ + private $manager; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @inheritDoc + */ + protected function setUp() + { + /** @var ObjectManager $objectManager */ + $objectManager = Bootstrap::getObjectManager(); + $this->manager = $objectManager->get(CodeLimitManager::class); + $this->customerSession = $objectManager->get(CustomerSession::class); + /** @var Http $request */ + $request = $objectManager->get(RequestInterface::class); + $request->getServer()->set('REMOTE_ADDR', '127.0.0.1'); + $objectManager->removeSharedInstance(RemoteAddress::class); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + $this->customerSession->logout(); + $this->customerSession->clearStorage(); + } + + /** + * Log in customer by ID. + * + * @param int $id + * @return void + */ + private function loginCustomer(int $id): void + { + if (!$this->customerSession->loginById($id)) { + throw new \RuntimeException('Failed to log in customer'); + } + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + */ + public function testCounterDisabled() + { + $this->manager->checkRequest('fakeCode1'); + $this->loginCustomer(1); + $this->manager->checkRequest('fakeCode2'); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms sales_rule_coupon_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 3 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 5 + * + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testUnderLimit() + { + $this->manager->checkRequest('fakeCode3'); + $this->manager->checkRequest('fakeCode4'); + + $this->loginCustomer(1); + $this->manager->checkRequest('fakeCode5'); + $this->manager->checkRequest('fakeCode6'); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms sales_rule_coupon_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 10 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 2 + * + * @expectedException \Magento\SalesRule\Api\Exception\CodeRequestLimitException + */ + public function testAboveLimitNotLoggedIn() + { + try { + $this->manager->checkRequest('fakeCode7'); + $this->manager->checkRequest('fakeCode8'); + } catch (CodeRequestLimitException $exception) { + $this->fail('Attempt denied before reaching the limit'); + } + $this->manager->checkRequest('fakeCode9'); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms sales_rule_coupon_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 2 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 10 + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Magento\SalesRule\Api\Exception\CodeRequestLimitException + */ + public function testAboveLimitLoggedIn() + { + try { + $this->loginCustomer(1); + $this->manager->checkRequest('fakeCode10'); + $this->manager->checkRequest('fakeCode11'); + } catch (CodeRequestLimitException $exception) { + $this->fail('Attempt denied before reaching the limit'); + } + $this->manager->checkRequest('fakeCode12'); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms sales_rule_coupon_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 10 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 10 + * @magentoConfigFixture default_store customer/captcha/mode always + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Magento\SalesRule\Api\Exception\CodeRequestLimitException + */ + public function testCustomerNotAllowedWithoutCode() + { + $this->loginCustomer(1); + $this->manager->checkRequest('fakeCode13'); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms sales_rule_coupon_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 10 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 10 + * @magentoConfigFixture default_store customer/captcha/mode always + * + * @expectedException \Magento\SalesRule\Api\Exception\CodeRequestLimitException + */ + public function testGuestNotAllowedWithoutCode() + { + $this->manager->checkRequest('fakeCode14'); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms sales_rule_coupon_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 2 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 10 + * + * @magentoDataFixture Magento/SalesRule/_files/rules.php + * @magentoDataFixture Magento/SalesRule/_files/coupons.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Magento\SalesRule\Api\Exception\CodeRequestLimitException + */ + public function testLoggingOnlyInvalidCodes() + { + try { + $this->loginCustomer(1); + $this->manager->checkRequest('coupon_code'); + $this->manager->checkRequest('coupon_code'); + $this->manager->checkRequest('fakeCode15'); + $this->manager->checkRequest('fakeCode16'); + } catch (CodeRequestLimitException $exception) { + $this->fail('Attempts are logged for existing codes'); + } + $this->manager->checkRequest('fakeCode17'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/QuoteRepositoryTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/QuoteRepositoryTest.php new file mode 100644 index 0000000000000..0b17a3b5522b9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/QuoteRepositoryTest.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon; + +use Magento\Captcha\Model\DefaultModel; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\SalesRule\Api\Exception\CodeRequestLimitException; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\TestFramework\ObjectManager; + +/** + * Test cases related to coupons. + */ +class QuoteRepositoryTest extends TestCase +{ + /** + * @var CartRepositoryInterface + */ + private $repo; + + /** + * @var SearchCriteriaBuilder + */ + private $criteriaBuilder; + + /** + * @var Http + */ + private $request; + + /** + * @var CaptchaHelper + */ + private $captchaHelper; + + /** + * @inheritDoc + */ + protected function setUp() + { + /** @var ObjectManager $objectManager */ + $objectManager = Bootstrap::getObjectManager(); + /** @var Http $request */ + $request = $objectManager->get(RequestInterface::class); + $request->getServer()->set('REMOTE_ADDR', '127.0.0.1'); + $this->request = $request; + $objectManager->removeSharedInstance(RemoteAddress::class); + $this->repo = $objectManager->get(CartRepositoryInterface::class); + $this->criteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->captchaHelper = $objectManager->get(CaptchaHelper::class); + } + + /** + * Load cart from fixture. + * + * @return CartInterface + */ + private function getCart(): CartInterface + { + $carts = $this->repo->getList( + $this->criteriaBuilder->addFilter('reserved_order_id', 'test01')->create() + )->getItems(); + if (!$carts) { + throw new \RuntimeException('Cart from fixture not found'); + } + + return array_shift($carts); + } + + /** + * Case when coupon requests limit is reached. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms sales_rule_coupon_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 10 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 2 + * + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/SalesRule/_files/coupon_cart_fixed_discount.php + * + * @expectedException \Magento\SalesRule\Api\Exception\CodeRequestLimitException + */ + public function testAboveLimitFail() + { + //Making number of requests above limit. + try { + $this->repo->save($this->getCart()->setCouponCode('fake20')); + $this->repo->save($this->getCart()->setCouponCode('fake21')); + } catch (CodeRequestLimitException $exception) { + $this->fail('Denied access before the limit is reached.'); + } + $this->repo->save($this->getCart()->setCouponCode('fake22')); + } + + /** + * Case when coupon requests limit reached but genuine request provided. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms sales_rule_coupon_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 10 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 2 + * + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/SalesRule/_files/coupon_cart_fixed_discount.php + */ + public function testAboveLimitSuccess() + { + $this->repo->save($this->getCart()->setCouponCode('fake24')); + $this->repo->save($this->getCart()->setCouponCode('fake25')); + + //Providing genuine proof. + /** @var DefaultModel $captcha */ + $captcha = $this->captchaHelper->getCaptcha('sales_rule_coupon_request'); + $captcha->generate(); + $this->request->setPostValue('captcha', ['sales_rule_coupon_request' => $captcha->getWord()]); + $this->repo->save($this->getCart()->setCouponCode('fake26')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php index 90540a1d637d5..2e5f77ec2865c 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php @@ -31,6 +31,9 @@ public function loadByPhraseDataProvider(): array [ 'ELIZABETH', [] ], + [ + '-+<(ELIZABETH)>*~', [] + ], [ 'ENGLISH', [['synonyms' => 'british,english', 'store_id' => 1, 'website_id' => 0]] ], @@ -43,6 +46,9 @@ public function loadByPhraseDataProvider(): array [ 'Monarch', [['synonyms' => 'queen,monarch', 'store_id' => 1, 'website_id' => 0]] ], + [ + '-+<(Monarch)>*~', [['synonyms' => 'queen,monarch', 'store_id' => 1, 'website_id' => 0]] + ], [ 'MONARCH English', [ ['synonyms' => 'queen,monarch', 'store_id' => 1, 'website_id' => 0], diff --git a/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php b/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php index b8ae7751a15ee..d6388b188a5fd 100644 --- a/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php @@ -17,7 +17,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase /** * Base product image path */ - const BASE_IMAGE_PATH = 'http://localhost/pub/media/catalog/product/cache/8d4d2075b1a30681853bef5bdc41b164'; + const BASE_IMAGE_PATH = '#http\:\/\/localhost\/pub\/media\/catalog\/product\/cache\/[a-z0-9]{32}:path:#'; /** * Test getCollection None images @@ -76,21 +76,21 @@ public function testGetCollectionAll() $this->assertEmpty($products[1]->getImages(), 'Images were loaded'); $this->assertNotEmpty($products[4]->getImages(), 'Images were not loaded'); $this->assertEquals('Simple Images', $products[4]->getImages()->getTitle(), 'Incorrect title'); - $this->assertEquals( - self::BASE_IMAGE_PATH.'/m/a/magento_image_sitemap.png', + $this->assertRegExp( + str_replace(':path:', preg_quote('/m/a/magento_image_sitemap.png'), self::BASE_IMAGE_PATH), $products[4]->getImages()->getThumbnail(), 'Incorrect thumbnail' ); $this->assertCount(2, $products[4]->getImages()->getCollection(), 'Not all images were loaded'); $imagesCollection = $products[4]->getImages()->getCollection(); - $this->assertEquals( - self::BASE_IMAGE_PATH.'/m/a/magento_image_sitemap.png', + $this->assertRegExp( + str_replace(':path:', preg_quote('/m/a/magento_image_sitemap.png'), self::BASE_IMAGE_PATH), $imagesCollection[0]->getUrl(), 'Incorrect image url' ); - $this->assertEquals( - self::BASE_IMAGE_PATH.'/s/e/second_image.png', + $this->assertRegExp( + str_replace(':path:', preg_quote('/s/e/second_image.png'), self::BASE_IMAGE_PATH), $imagesCollection[1]->getUrl(), 'Incorrect image url' ); @@ -101,13 +101,13 @@ public function testGetCollectionAll() $this->assertEquals('no_selection', $products[5]->getThumbnail(), 'thumbnail is incorrect'); $imagesCollection = $products[5]->getImages()->getCollection(); $this->assertCount(1, $imagesCollection); - $this->assertEquals( - self::BASE_IMAGE_PATH.'/s/e/second_image_1.png', + $this->assertRegExp( + str_replace(':path:', preg_quote('/s/e/second_image_1.png'), self::BASE_IMAGE_PATH), $imagesCollection[0]->getUrl(), 'Image url is incorrect' ); - $this->assertEquals( - self::BASE_IMAGE_PATH.'/s/e/second_image_1.png', + $this->assertRegExp( + str_replace(':path:', preg_quote('/s/e/second_image_1.png'), self::BASE_IMAGE_PATH), $products[5]->getImages()->getThumbnail(), 'Product thumbnail is incorrect' ); @@ -144,16 +144,16 @@ public function testGetCollectionBase() $this->assertEmpty($products[1]->getImages(), 'Images were loaded'); $this->assertNotEmpty($products[4]->getImages(), 'Images were not loaded'); $this->assertEquals('Simple Images', $products[4]->getImages()->getTitle(), 'Incorrect title'); - $this->assertEquals( - self::BASE_IMAGE_PATH.'/s/e/second_image.png', + $this->assertRegExp( + str_replace(':path:', preg_quote('/s/e/second_image.png'), self::BASE_IMAGE_PATH), $products[4]->getImages()->getThumbnail(), 'Incorrect thumbnail' ); $this->assertCount(1, $products[4]->getImages()->getCollection(), 'Number of loaded images is incorrect'); $imagesCollection = $products[4]->getImages()->getCollection(); - $this->assertEquals( - self::BASE_IMAGE_PATH.'/s/e/second_image.png', + $this->assertRegExp( + str_replace(':path:', preg_quote('/s/e/second_image.png'), self::BASE_IMAGE_PATH), $imagesCollection[0]->getUrl(), 'Incorrect image url' ); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php index f806674d29705..758a8331cddcb 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php @@ -41,7 +41,7 @@ protected function setUp() */ private function getRandomColor() : string { - return '#' . str_pad(dechex(mt_rand(0, 0xFFFFFF)), 6, '0', STR_PAD_LEFT); + return '#' . str_pad(dechex(random_int(0, 0xFFFFFF)), 6, '0', STR_PAD_LEFT); } /** diff --git a/dev/tests/integration/testsuite/Magento/Ui/Controller/Index/RenderTest.php b/dev/tests/integration/testsuite/Magento/Ui/Controller/Index/RenderTest.php new file mode 100644 index 0000000000000..c63a6fe75f5fc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ui/Controller/Index/RenderTest.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Ui\Controller\Index; + +use Magento\TestFramework\TestCase\AbstractController; +use Zend\Http\Headers; + +/** + * Test component rendering on storefront. + * + * @magentoAppArea frontend + */ +class RenderTest extends AbstractController +{ + /** + * Test content type being chosen based on context. + */ + public function testContentType() + { + $this->getRequest()->setParam('namespace', 'widget_recently_viewed'); + $this->getRequest()->setHeaders(Headers::fromString('Accept: application/json')); + $this->dispatch('mui/index/render'); + $this->assertNotEmpty($contentType = $this->getResponse()->getHeader('Content-Type')); + $this->assertEquals('application/json', $contentType->getFieldValue()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/GridTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/GridTest.php index 69265f33b305a..ccd25f64a4bdd 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/GridTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/GridTest.php @@ -9,6 +9,8 @@ /** * Testing the list of locked users. + * + * @magentoAppArea adminhtml */ class GridTest extends AbstractBackendController { diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/IndexTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/IndexTest.php index 59cb49c7831eb..4519e52f0678b 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/IndexTest.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\User\Controller\Adminhtml\Locks; /** - * Testing locked users list. + * Locked users page test. + * + * @magentoAppArea adminhtml */ class IndexTest extends \Magento\TestFramework\TestCase\AbstractBackendController { diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/MassUnlockTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/MassUnlockTest.php index bd83f202d6942..723a4c481b864 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/MassUnlockTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/Locks/MassUnlockTest.php @@ -5,6 +5,11 @@ */ namespace Magento\User\Controller\Adminhtml\Locks; +/** + * Testing unlock controller. + * + * @magentoAppArea adminhtml + */ class MassUnlockTest extends \Magento\TestFramework\TestCase\AbstractBackendController { /** diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php index 937a26fdf0a89..d4cb7a1cf4e20 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php @@ -11,6 +11,8 @@ /** * Test class for Magento\User\Controller\Adminhtml\User\InvalidateToken. + * + * @magentoAppArea adminhtml */ class InvalidateTokenTest extends \Magento\TestFramework\TestCase\AbstractBackendController { diff --git a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php index 8b85339afd789..a6fc9999ad267 100644 --- a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php @@ -7,6 +7,7 @@ namespace Magento\User\Model; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Encryption\Encryptor; /** * @magentoAppArea adminhtml @@ -33,6 +34,11 @@ class UserTest extends \PHPUnit\Framework\TestCase */ private $serializer; + /** + * @var Encryptor + */ + private $encryptor; + protected function setUp() { $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -44,6 +50,9 @@ protected function setUp() $this->serializer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( Json::class ); + $this->encryptor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + Encryptor::class + ); } /** @@ -104,6 +113,9 @@ public function testUpdateRoleOnSave() $this->assertEquals('admin_role', $this->_model->getRole()->getRoleName()); } + /** + * phpcs:disable Magento2.Functions.StaticFunction + */ public static function roleDataFixture() { self::$_newRole = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -335,6 +347,9 @@ public function testBeforeSaveRequiredFieldsValidation() */ public function testBeforeSavePasswordHash() { + $pattern = $this->encryptor->getLatestHashVersion() === Encryptor::HASH_VERSION_ARGON2ID13 ? + '/^[0-9a-f]+:[0-9a-zA-Z]{16}:[0-9]+$/' : + '/^[0-9a-f]+:[0-9a-zA-Z]{32}:[0-9]+$/'; $this->_model->setUsername( 'john.doe' )->setFirstname( @@ -349,7 +364,7 @@ public function testBeforeSavePasswordHash() $this->_model->save(); $this->assertNotContains('123123q', $this->_model->getPassword(), 'Password is expected to be hashed'); $this->assertRegExp( - '/^[0-9a-f]+:[0-9a-zA-Z]{32}:[0-9]+$/', + $pattern, $this->_model->getPassword(), 'Salt is expected to be saved along with the password' ); diff --git a/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapi.php b/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapi.php index 1e5b338e0a7ef..57c8fbf45c63c 100644 --- a/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapi.php +++ b/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapi.php @@ -12,12 +12,16 @@ 'Magento_TestModuleMSC::resource1', ], 'secure' => false, + 'realMethod' => 'item', + 'parameters' => [] ], 'create' => [ 'resources' => [ 'Magento_TestModuleMSC::resource3', ], 'secure' => false, + 'realMethod' => 'create', + 'parameters' => [] ], ], ], @@ -29,6 +33,8 @@ 'Magento_TestModuleMSC::resource2', ], 'secure' => false, + 'realMethod' => 'getPreconfiguredItem', + 'parameters' => [] ], ], ], @@ -40,12 +46,34 @@ 'Magento_Test1::resource1', ], 'secure' => false, + 'realMethod' => 'item', + 'parameters' => [] + ], + 'itemDefault' => [ + 'resources' => [ + 'Magento_Test1::default', + ], + 'secure' => false, + 'realMethod' => 'item', + 'parameters' => [ + 'id' => [ + 'force' => true, + 'value' => null, + ], + ] ], 'create' => [ 'resources' => [ 'Magento_Test1::resource1', ], 'secure' => false, + 'realMethod' => 'create', + 'parameters' => [ + 'id' => [ + 'force' => true, + 'value' => null, + ], + ] ], ], ], @@ -58,6 +86,8 @@ 'Magento_Test1::resource2', ], 'secure' => false, + 'realMethod' => 'item', + 'parameters' => [] ], 'create' => [ 'resources' => [ @@ -65,6 +95,13 @@ 'Magento_Test1::resource2', ], 'secure' => false, + 'realMethod' => 'create', + 'parameters' => [ + 'id' => [ + 'force' => true, + 'value' => null, + ], + ] ], 'delete' => [ 'resources' => [ @@ -72,6 +109,8 @@ 'Magento_Test1::resource2', ], 'secure' => false, + 'realMethod' => 'delete', + 'parameters' => [] ], 'update' => [ 'resources' => [ @@ -79,6 +118,8 @@ 'Magento_Test1::resource2', ], 'secure' => false, + 'realMethod' => 'update', + 'parameters' => [] ], ], ], @@ -127,25 +168,46 @@ ], ], ], - '/V2/testmodule1/:id' => [ + '/V1/testmodule1' => [ 'GET' => [ 'secure' => false, 'service' => [ - 'class' => \Magento\TestModule1\Service\V2\AllSoapAndRestInterface::class, + 'class' => \Magento\TestModule1\Service\V1\AllSoapAndRestInterface::class, 'method' => 'item', ], + 'resources' => [ + 'Magento_Test1::default' => true, + ], + 'parameters' => [ + 'id' => [ + 'force' => true, + 'value' => null, + ], + ], + ], + 'POST' => [ + 'secure' => false, + 'service' => [ + 'class' => \Magento\TestModule1\Service\V1\AllSoapAndRestInterface::class, + 'method' => 'create', + ], 'resources' => [ 'Magento_Test1::resource1' => true, - 'Magento_Test1::resource2' => true, ], 'parameters' => [ + 'id' => [ + 'force' => true, + 'value' => null, + ], ], ], - 'DELETE' => [ + ], + '/V2/testmodule1/:id' => [ + 'GET' => [ 'secure' => false, 'service' => [ 'class' => \Magento\TestModule1\Service\V2\AllSoapAndRestInterface::class, - 'method' => 'delete', + 'method' => 'item', ], 'resources' => [ 'Magento_Test1::resource1' => true, @@ -154,11 +216,11 @@ 'parameters' => [ ], ], - 'PUT' => [ + 'DELETE' => [ 'secure' => false, 'service' => [ 'class' => \Magento\TestModule1\Service\V2\AllSoapAndRestInterface::class, - 'method' => 'update', + 'method' => 'delete', ], 'resources' => [ 'Magento_Test1::resource1' => true, @@ -167,35 +229,30 @@ 'parameters' => [ ], ], - ], - '/V2/testmodule1' => [ - 'POST' => [ + 'PUT' => [ 'secure' => false, 'service' => [ 'class' => \Magento\TestModule1\Service\V2\AllSoapAndRestInterface::class, - 'method' => 'create', + 'method' => 'update', ], 'resources' => [ 'Magento_Test1::resource1' => true, 'Magento_Test1::resource2' => true, ], 'parameters' => [ - 'id' => [ - 'force' => true, - 'value' => null, - ], ], ], ], - '/V1/testmodule1' => [ + '/V2/testmodule1' => [ 'POST' => [ 'secure' => false, 'service' => [ - 'class' => \Magento\TestModule1\Service\V1\AllSoapAndRestInterface::class, + 'class' => \Magento\TestModule1\Service\V2\AllSoapAndRestInterface::class, 'method' => 'create', ], 'resources' => [ 'Magento_Test1::resource1' => true, + 'Magento_Test1::resource2' => true, ], 'parameters' => [ 'id' => [ diff --git a/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapiA.xml b/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapiA.xml index c814b1039b04b..389908309b2a2 100644 --- a/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapiA.xml +++ b/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapiA.xml @@ -13,6 +13,15 @@ <resource ref="Magento_Test1::resource1"/> </resources> </route> + <route url="/V1/testmodule1" method="GET" soapOperation="itemDefault"> + <service class="Magento\TestModule1\Service\V1\AllSoapAndRestInterface" method="item"/> + <resources> + <resource ref="Magento_Test1::default"/> + </resources> + <data> + <parameter name="id" force="true">null</parameter> + </data> + </route> <route url="/V2/testmodule1/:id" method="GET"> <service class="Magento\TestModule1\Service\V2\AllSoapAndRestInterface" method="item"/> <resources> diff --git a/dev/tests/integration/testsuite/Magento/Webapi/Model/Soap/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Webapi/Model/Soap/ConfigTest.php index 1236ad20c3486..8a9184f5d5e4c 100644 --- a/dev/tests/integration/testsuite/Magento/Webapi/Model/Soap/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Webapi/Model/Soap/ConfigTest.php @@ -64,7 +64,8 @@ public function testGetRequestedSoapServices() '\\' . LocalizedException::class ] ] - ] + ], + 'parameters' => [] ] ], 'class' => AccountManagementInterface::class, @@ -88,8 +89,8 @@ public function testGetServiceMethodInfo() 'isSecure' => false, 'resources' => [ 'Magento_Customer::customer', - 'self' ], + 'parameters' => [] ]; $actual = $this->soapConfig->getServiceMethodInfo( 'customerCustomerRepositoryV1GetById', diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/SerializationAware.php b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/SerializationAware.php deleted file mode 100644 index e38fba8558bad..0000000000000 --- a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/SerializationAware.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\CodeMessDetector\Rule\Design; - -use PHPMD\AbstractNode; -use PHPMD\AbstractRule; -use PHPMD\Node\ClassNode; -use PHPMD\Node\MethodNode; -use PDepend\Source\AST\ASTMethod; -use PHPMD\Rule\MethodAware; - -/** - * Detect PHP serialization aware methods. - */ -class SerializationAware extends AbstractRule implements MethodAware -{ - /** - * @inheritDoc - * - * @param ASTMethod|MethodNode $method - */ - public function apply(AbstractNode $method) - { - if ($method->getName() === '__wakeup' || $method->getName() === '__sleep') { - $this->addViolation($method, [$method->getName(), $method->getParent()->getFullQualifiedName()]); - } - } -} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml index 5f2461812bab7..53f2fe4a0084e 100644 --- a/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml +++ b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml @@ -60,31 +60,6 @@ class OrderProcessor $currentOrder = $this->session->get('current_order'); ... } -} - ]]> - </example> - </rule> - <rule name="SerializationAware" - class="Magento\CodeMessDetector\Rule\Design\SerializationAware" - message="{1} has {0} method and is PHP serialization aware - PHP serialization must be avoided."> - <description> - <![CDATA[ -Using PHP serialization must be avoided in Magento for security reasons and for prevention of unexpected behaviour. - ]]> - </description> - <priority>2</priority> - <properties /> - <example> - <![CDATA[ -class MyModel extends AbstractModel -{ - - ....... - - public function __sleep() - { - ..... - } } ]]> </example> diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/ConfigTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/ConfigTest.php index 85a2d286eac95..c6d90fd55af93 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/ConfigTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/ConfigTest.php @@ -12,6 +12,9 @@ */ class ConfigTest extends AbstractConfig { + /** + * @inheritdoc + */ public function testSchemaUsingInvalidXml($expectedErrors = null) { // @codingStandardsIgnoreStart @@ -27,6 +30,22 @@ public function testSchemaUsingInvalidXml($expectedErrors = null) parent::testSchemaUsingInvalidXml($expectedErrors); } + /** + * @inheritdoc + */ + public function testFileSchemaUsingInvalidXml($expectedErrors = null) + { + // @codingStandardsIgnoreStart + $expectedErrors = [ + "Element 'route', attribute 'method': [facet 'enumeration'] The value 'PATCH' is not an element of the set {'GET', 'PUT', 'POST', 'DELETE'}.", + "Element 'route', attribute 'method': 'PATCH' is not a valid value of the local atomic type.", + "Element 'service': The attribute 'method' is required but missing.", + "Element 'data': Missing child element(s). Expected is ( parameter ).", + ]; + // @codingStandardsIgnoreEnd + parent::testFileSchemaUsingInvalidXml($expectedErrors); + } + /** * Returns the name of the xml files to validate * @@ -64,7 +83,7 @@ protected function _getKnownInvalidXml() */ protected function _getKnownValidPartialXml() { - return null; + return __DIR__ . '/_files/partial_webapi.xml'; } /** @@ -74,7 +93,7 @@ protected function _getKnownValidPartialXml() */ protected function _getKnownInvalidPartialXml() { - return null; + return __DIR__ . '/_files/partial_invalid_webapi.xml'; } /** @@ -85,7 +104,7 @@ protected function _getKnownInvalidPartialXml() protected function _getXsd() { $urnResolver = new \Magento\Framework\Config\Dom\UrnResolver(); - return $urnResolver->getRealPath('urn:magento:module:Magento_Webapi:etc/webapi.xsd'); + return $urnResolver->getRealPath('urn:magento:module:Magento_Webapi:etc/webapi_merged.xsd'); } /** @@ -95,6 +114,7 @@ protected function _getXsd() */ protected function _getFileXsd() { - return null; + $urnResolver = new \Magento\Framework\Config\Dom\UrnResolver(); + return $urnResolver->getRealPath('urn:magento:module:Magento_Webapi:etc/webapi.xsd'); } } diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/_files/partial_invalid_webapi.xml b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/_files/partial_invalid_webapi.xml new file mode 100644 index 0000000000000..7761eac3fbdb0 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/_files/partial_invalid_webapi.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd"> + <route url="/V1/customer/me" method="PATCH"> + <service class="Magento\Customer\Api\CustomerRepositoryInterface" /> + <resources> + <resource ref="a resource" /> + </resources> + <data> + </data> + </route> + <route url="/V1/customers" method="POST"/> + <route url="/V1/customers" method="PUT"> + <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="foo" /> + </route> +</routes> diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/_files/partial_webapi.xml b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/_files/partial_webapi.xml new file mode 100644 index 0000000000000..00fc8f16d85cc --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/_files/partial_webapi.xml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd"> + <route url="/V1/customers/me" method="GET"> + <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="getById" /> + <resources> + <resource ref="Magento_Customer::customer_self" /> + </resources> + <data> + <parameter name="id" force="true">null</parameter> + </data> + </route> + <route url="/V1/customers/me" method="PUT" secure="true"> + <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="save" /> + <resources> + <resource ref="Magento_Customer::customer_self" /> + </resources> + <data> + <parameter name="id">null</parameter> + </data> + </route> + <route url="/V1/customers" method="POST"> + <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="save" /> + <resources> + <resource ref="Magento_Customer::manage" /> + </resources> + </route> + <route url="/V1/customers/:id" method="GET"> + <resources> + <resource ref="Magento_Customer::read" /> + </resources> + </route> +</routes> diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml index e65a9a089da9e..0e3b5fa3d341c 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml @@ -45,6 +45,5 @@ <!-- Magento Specific Rules --> <rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/AllPurposeAction" /> <rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/CookieAndSessionMisuse" /> - <rule ref="Magento/CodeMessDetector/resources/rulesets/design.xml/SerializationAware" /> </ruleset> diff --git a/dev/tools/UpgradeScripts/pre_composer_update_2.3.php b/dev/tools/UpgradeScripts/pre_composer_update_2.3.php index 04e64ba45ba0e..3b3f575632124 100644 --- a/dev/tools/UpgradeScripts/pre_composer_update_2.3.php +++ b/dev/tools/UpgradeScripts/pre_composer_update_2.3.php @@ -233,12 +233,16 @@ output('\nUpdating "require-dev" section of composer.json'); runComposer('require --dev ' . - 'phpunit/phpunit:~6.2.0 ' . - 'friendsofphp/php-cs-fixer:~2.10.1 ' . + 'allure-framework/allure-phpunit:~1.2.0 ' . + 'friendsofphp/php-cs-fixer:~2.13.0 ' . 'lusitanian/oauth:~0.8.10 ' . + 'magento/magento-coding-standard:~1.0.0 ' . + 'magento/magento2-functional-testing-framework:~2.3.14 ' . 'pdepend/pdepend:2.5.2 ' . + 'phpunit/phpunit:~6.5.0 ' . 'sebastian/phpcpd:~3.0.0 ' . - 'squizlabs/php_codesniffer:3.2.2 --no-update'); + 'squizlabs/php_codesniffer:3.3.1 ' . + '--sort-packages --no-update'); output(''); runComposer('remove --dev sjparkinson/static-review fabpot/php-cs-fixer --no-update'); diff --git a/lib/internal/Magento/Framework/App/Action/HttpGetActionInterface.php b/lib/internal/Magento/Framework/App/Action/HttpGetActionInterface.php index 308b77aa8dbcf..c3d3d2d6fd5ec 100644 --- a/lib/internal/Magento/Framework/App/Action/HttpGetActionInterface.php +++ b/lib/internal/Magento/Framework/App/Action/HttpGetActionInterface.php @@ -8,12 +8,10 @@ namespace Magento\Framework\App\Action; -use Magento\Framework\App\ActionInterface; - /** * Marker for actions processing GET requests. */ -interface HttpGetActionInterface extends ActionInterface +interface HttpGetActionInterface extends HttpHeadActionInterface { } diff --git a/lib/internal/Magento/Framework/App/AreaList/Proxy.php b/lib/internal/Magento/Framework/App/AreaList/Proxy.php index 09115add57190..d080e4cabbd87 100644 --- a/lib/internal/Magento/Framework/App/AreaList/Proxy.php +++ b/lib/internal/Magento/Framework/App/AreaList/Proxy.php @@ -3,11 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\App\AreaList; /** - * Proxy for area list. + * Application area list */ class Proxy extends \Magento\Framework\App\AreaList implements \Magento\Framework\ObjectManager\NoninterceptableInterface @@ -58,17 +57,12 @@ public function __construct( } /** - * Remove links to other objects. + * Sleep magic method. * * @return array - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return ['_subject', '_isShared']; } @@ -76,14 +70,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } diff --git a/lib/internal/Magento/Framework/App/Config/Initial/Converter.php b/lib/internal/Magento/Framework/App/Config/Initial/Converter.php index 8f1a8e9f298e7..67b671a7b1a31 100644 --- a/lib/internal/Magento/Framework/App/Config/Initial/Converter.php +++ b/lib/internal/Magento/Framework/App/Config/Initial/Converter.php @@ -7,6 +7,9 @@ */ namespace Magento\Framework\App\Config\Initial; +/** + * Class Converter + */ class Converter implements \Magento\Framework\Config\ConverterInterface { /** diff --git a/lib/internal/Magento/Framework/App/Config/MetadataConfigTypeProcessor.php b/lib/internal/Magento/Framework/App/Config/MetadataConfigTypeProcessor.php index bc23032903d23..56fc064255015 100644 --- a/lib/internal/Magento/Framework/App/Config/MetadataConfigTypeProcessor.php +++ b/lib/internal/Magento/Framework/App/Config/MetadataConfigTypeProcessor.php @@ -126,10 +126,13 @@ private function processScopeData( //Failed to load scopes or config source, perhaps config data received is outdated. return $data; } - /** @var \Magento\Framework\App\Config\Data\ProcessorInterface $processor */ - $processor = $this->_processorFactory->get($metadata['backendModel']); - $value = $processor->processValue($this->_getValue($data, $path)); - $this->_setValue($data, $path, $value); + + if (isset($metadata['backendModel'])) { + /** @var \Magento\Framework\App\Config\Data\ProcessorInterface $processor */ + $processor = $this->_processorFactory->get($metadata['backendModel']); + $value = $processor->processValue($this->_getValue($data, $path)); + $this->_setValue($data, $path, $value); + } } return $data; diff --git a/lib/internal/Magento/Framework/App/Request/CsrfValidator.php b/lib/internal/Magento/Framework/App/Request/CsrfValidator.php index c930fc920907c..edbf7532551d2 100644 --- a/lib/internal/Magento/Framework/App/Request/CsrfValidator.php +++ b/lib/internal/Magento/Framework/App/Request/CsrfValidator.php @@ -55,8 +55,10 @@ public function __construct( } /** + * Validate given request. + * * @param HttpRequest $request - * @param ActionInterface $action + * @param ActionInterface $action * * @return bool */ @@ -70,7 +72,7 @@ private function validateRequest( } if ($valid === null) { $valid = !$request->isPost() - || $request->isAjax() + || $request->isXmlHttpRequest() || $this->formKeyValidator->validate($request); } @@ -78,6 +80,8 @@ private function validateRequest( } /** + * Create exception for when incoming request failed validation. + * * @param HttpRequest $request * @param ActionInterface $action * diff --git a/lib/internal/Magento/Framework/App/Response/Http.php b/lib/internal/Magento/Framework/App/Response/Http.php index a80d9cbdd6689..e6fff90837d9d 100644 --- a/lib/internal/Magento/Framework/App/Response/Http.php +++ b/lib/internal/Magento/Framework/App/Response/Http.php @@ -1,9 +1,10 @@ <?php /** + * HTTP response + * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\App\Response; use Magento\Framework\App\Http\Context; @@ -16,7 +17,7 @@ use Magento\Framework\Session\Config\ConfigInterface; /** - * HTTP Response. + * HTTP response * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ @@ -99,7 +100,9 @@ public function setXFrameOptions($value) /** * Send Vary cookie * - * @return void + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Stdlib\Cookie\CookieSizeLimitReachedException + * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException */ public function sendVary() { @@ -117,9 +120,9 @@ public function sendVary() } /** - * Set headers for public cache. + * Set headers for public cache * - * Also accepts the time-to-live (max-age) parameter. + * Accepts the time-to-live (max-age) parameter * * @param int $ttl * @return void @@ -179,18 +182,13 @@ public function representJson($content) } /** - * Remove links to other objects. + * Sleep magic method. * * @return string[] * @codeCoverageIgnore - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return ['content', 'isRedirect', 'statusCode', 'context', 'headers']; } @@ -199,14 +197,9 @@ public function __sleep() * * @return void * @codeCoverageIgnore - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $objectManager = ObjectManager::getInstance(); $this->cookieManager = $objectManager->create(\Magento\Framework\Stdlib\CookieManagerInterface::class); $this->cookieMetadataFactory = $objectManager->get( diff --git a/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php b/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php index 5e79315238f7d..09dda9727b937 100644 --- a/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php +++ b/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php @@ -60,17 +60,12 @@ public function __construct( } /** - * Remove links to other objects. + * Sleep magic method. * * @return array - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return ['_subject', '_isShared']; } @@ -78,14 +73,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } diff --git a/lib/internal/Magento/Framework/App/Router/ActionList.php b/lib/internal/Magento/Framework/App/Router/ActionList.php index 9944e617b1ce6..1640d4a98d354 100644 --- a/lib/internal/Magento/Framework/App/Router/ActionList.php +++ b/lib/internal/Magento/Framework/App/Router/ActionList.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,6 +9,9 @@ use Magento\Framework\Serialize\Serializer\Serialize; use Magento\Framework\Module\Dir\Reader as ModuleReader; +/** + * Class to retrieve action class. + */ class ActionList { /** @@ -91,6 +93,7 @@ public function get($module, $area, $namespace, $action) if ($area) { $area = '\\' . $area; } + $namespace = strtolower($namespace); if (strpos($namespace, self::NOT_ALLOWED_IN_NAMESPACE_PATH) !== false) { return null; } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php index 9be68b379900a..efb35b7321c3b 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php @@ -290,6 +290,45 @@ public function testRepresentJson() $this->assertEquals('json_string', $this->model->getBody('default')); } + /** + * + * @expectedException \RuntimeException + * @expectedExceptionMessage ObjectManager isn't initialized + */ + public function testWakeUpWithException() + { + /* ensure that the test preconditions are met */ + $objectManagerClass = new \ReflectionClass(\Magento\Framework\App\ObjectManager::class); + $instanceProperty = $objectManagerClass->getProperty('_instance'); + $instanceProperty->setAccessible(true); + $instanceProperty->setValue(null); + + $this->model->__wakeup(); + $this->assertNull($this->cookieMetadataFactoryMock); + $this->assertNull($this->cookieManagerMock); + } + + /** + * Test for the magic method __wakeup + * + * @covers \Magento\Framework\App\Response\Http::__wakeup + */ + public function testWakeUpWith() + { + $objectManagerMock = $this->createMock(\Magento\Framework\App\ObjectManager::class); + $objectManagerMock->expects($this->once()) + ->method('create') + ->with(\Magento\Framework\Stdlib\CookieManagerInterface::class) + ->will($this->returnValue($this->cookieManagerMock)); + $objectManagerMock->expects($this->at(1)) + ->method('get') + ->with(\Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class) + ->will($this->returnValue($this->cookieMetadataFactoryMock)); + + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + $this->model->__wakeup(); + } + public function testSetXFrameOptions() { $value = 'DENY'; diff --git a/lib/internal/Magento/Framework/Config/Dom.php b/lib/internal/Magento/Framework/Config/Dom.php index 5c97c996634dd..e36f9615db26b 100644 --- a/lib/internal/Magento/Framework/Config/Dom.php +++ b/lib/internal/Magento/Framework/Config/Dom.php @@ -379,7 +379,7 @@ public static function validateDomDocument( libxml_set_external_entity_loader([self::$urnResolver, 'registerEntityLoader']); $errors = []; try { - $result = $dom->schemaValidate($schema, LIBXML_SCHEMA_CREATE); + $result = $dom->schemaValidate($schema); if (!$result) { $errors = self::getXmlErrors($errorFormat); } diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php b/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php index 73968ac1ed897..4d84be1ba4fc1 100644 --- a/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php +++ b/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php @@ -169,48 +169,6 @@ public function validateDataProvider() ]; } - /** - * @param string $xml - * @param string $expectedValue - * @dataProvider validateWithDefaultValueDataProvider - */ - public function testValidateWithDefaultValue($xml, $expectedValue) - { - if (!function_exists('libxml_set_external_entity_loader')) { - $this->markTestSkipped('Skipped on HHVM. Will be fixed in MAGETWO-45033'); - } - - $actualErrors = []; - - $dom = new \Magento\Framework\Config\Dom($xml, $this->validationStateMock); - $dom->validate(__DIR__ . '/_files/sample.xsd', $actualErrors); - - $actualValue = $dom->getDom() - ->getElementsByTagName('root')->item(0) - ->getElementsByTagName('node')->item(0) - ->getAttribute('attribute_with_default_value'); - - $this->assertEmpty($actualErrors); - $this->assertEquals($expectedValue, $actualValue); - } - - /** - * @return array - */ - public function validateWithDefaultValueDataProvider() - { - return [ - 'default_value' => [ - '<root><node id="id1"/></root>', - 'default_value' - ], - 'custom_value' => [ - '<root><node id="id1" attribute_with_default_value="non_default_value"/></root>', - 'non_default_value' - ], - ]; - } - public function testValidateCustomErrorFormat() { $xml = '<root><unknown_node/></root>'; diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd b/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd index 701a2eb18c2a1..1f635b7081e05 100644 --- a/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd @@ -21,7 +21,6 @@ <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="id" type="xs:string" use="required"/> - <xs:attribute name="attribute_with_default_value" type="xs:string" default="default_value"/> </xs:extension> </xs:simpleContent> </xs:complexType> diff --git a/lib/internal/Magento/Framework/DB/Helper/Mysql/Fulltext.php b/lib/internal/Magento/Framework/DB/Helper/Mysql/Fulltext.php index 8063c60143e00..5c50faf71a854 100644 --- a/lib/internal/Magento/Framework/DB/Helper/Mysql/Fulltext.php +++ b/lib/internal/Magento/Framework/DB/Helper/Mysql/Fulltext.php @@ -3,12 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\DB\Helper\Mysql; use Magento\Framework\App\ResourceConnection; +/** + * MySQL Fulltext Query Builder + */ class Fulltext { + /** + * Characters that have special meaning in fulltext match syntax + * + * @var string + */ + const SPECIAL_CHARACTERS = '-+<>*()~'; + /** * FULLTEXT search in MySQL search mode "natural language" */ @@ -73,8 +85,7 @@ public function getMatchQuery($columns, $expression, $mode = self::FULLTEXT_MODE } /** - * Method for FULLTEXT search in Mysql, will added generated - * MATCH ($columns) AGAINST ('$expression' $mode) to where clause + * Method for FULLTEXT search in Mysql; will add generated MATCH ($columns) AGAINST ('$expression' $mode) to $select * * @param \Magento\Framework\DB\Select $select * @param string|string[] $columns Columns which add to MATCH () @@ -95,4 +106,15 @@ public function match($select, $columns, $expression, $isCondition = true, $mode return $select; } + + /** + * Remove special characters from fulltext query expression + * + * @param string $expression + * @return string + */ + public function removeSpecialCharacters(string $expression): string + { + return str_replace(str_split(static::SPECIAL_CHARACTERS), '', $expression); + } } diff --git a/lib/internal/Magento/Framework/DB/Select.php b/lib/internal/Magento/Framework/DB/Select.php index 273d940babb91..7399845215bb5 100644 --- a/lib/internal/Magento/Framework/DB/Select.php +++ b/lib/internal/Magento/Framework/DB/Select.php @@ -400,7 +400,7 @@ public function useStraightJoin($flag = true) /** * Render STRAIGHT_JOIN clause * - * @param string $sql SQL query + * @param string $sql SQL query * @return string */ protected function _renderStraightjoin($sql) @@ -452,7 +452,7 @@ public function orderRand($field = null) /** * Render FOR UPDATE clause * - * @param string $sql SQL query + * @param string $sql SQL query * @return string */ protected function _renderForupdate($sql) @@ -509,18 +509,13 @@ public function assemble() } /** - * Remove links to other objects. + * Sleep magic method. * * @return string[] * @since 100.0.11 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $properties = array_keys(get_object_vars($this)); $properties = array_diff( $properties, @@ -537,14 +532,9 @@ public function __sleep() * * @return void * @since 100.0.11 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_adapter = $objectManager->get(ResourceConnection::class)->getConnection(); $this->selectRenderer = $objectManager->get(\Magento\Framework\DB\Select\SelectRenderer::class); diff --git a/lib/internal/Magento/Framework/DB/Select/RendererProxy.php b/lib/internal/Magento/Framework/DB/Select/RendererProxy.php index dc69b96b79050..b6d0803759842 100644 --- a/lib/internal/Magento/Framework/DB/Select/RendererProxy.php +++ b/lib/internal/Magento/Framework/DB/Select/RendererProxy.php @@ -56,17 +56,12 @@ public function __construct( } /** - * Remove links to other objects. + * Sleep magic method. * * @return array - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return ['_subject', '_isShared']; } @@ -74,14 +69,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } @@ -111,7 +101,7 @@ protected function _getSubject() } /** - * @inheritdoc + * @inheritDoc */ public function render(\Magento\Framework\DB\Select $select, $sql = '') { diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/Ddl/TriggerTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/Ddl/TriggerTest.php index f3d84b83ce053..f4dc5f95b5876 100644 --- a/lib/internal/Magento/Framework/DB/Test/Unit/Ddl/TriggerTest.php +++ b/lib/internal/Magento/Framework/DB/Test/Unit/Ddl/TriggerTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\DB\Test\Unit\Ddl; +/** + * Class TriggerTest + */ class TriggerTest extends \PHPUnit\Framework\TestCase { /** @@ -47,7 +50,7 @@ public function testGetListOfTimes() */ public function testGetNameWithSetName() { - $triggerName = 'TEST_TRIGGER_NAME' . mt_rand(100, 999); + $triggerName = 'TEST_TRIGGER_NAME' . random_int(100, 999); $this->_object->setName($triggerName); $this->assertEquals(strtolower($triggerName), $this->_object->getName()); @@ -101,7 +104,7 @@ public function testSetTableName() */ public function testGetNameWithException() { - $tableName = 'TEST_TABLE_NAME_' . mt_rand(100, 999); + $tableName = 'TEST_TABLE_NAME_' . random_int(100, 999); $event = \Magento\Framework\DB\Ddl\Trigger::EVENT_INSERT; $this->_object->setTable($tableName)->setTime(\Magento\Framework\DB\Ddl\Trigger::TIME_AFTER)->setEvent($event); @@ -117,7 +120,7 @@ public function testGetNameWithException() */ public function testGetTimeWithException() { - $tableName = 'TEST_TABLE_NAME_' . mt_rand(100, 999); + $tableName = 'TEST_TABLE_NAME_' . random_int(100, 999); $event = \Magento\Framework\DB\Ddl\Trigger::EVENT_INSERT; $this->_object->setTable($tableName)->setEvent($event); @@ -148,7 +151,7 @@ public function testGetTableWithException() */ public function testGetEventWithException() { - $tableName = 'TEST_TABLE_NAME_' . mt_rand(100, 999); + $tableName = 'TEST_TABLE_NAME_' . random_int(100, 999); $this->_object->setTable($tableName)->setTime(\Magento\Framework\DB\Ddl\Trigger::TIME_AFTER); diff --git a/lib/internal/Magento/Framework/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index 2f3aaad98dfe5..128d3d8e9fd3d 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -889,14 +889,9 @@ public function hasFlag($flag) * * @return string[] * @since 100.0.11 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $properties = array_keys(get_object_vars($this)); $properties = array_diff( $properties, @@ -912,14 +907,9 @@ public function __sleep() * * @return void * @since 100.0.11 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_entityFactory = $objectManager->get(EntityFactoryInterface::class); } diff --git a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php index 6082c2c6c5205..8a22c9a1ce4fc 100644 --- a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php +++ b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php @@ -733,7 +733,7 @@ public function loadData($printQuery = false, $logQuery = false) public function printLogQuery($printQuery = false, $logQuery = false, $sql = null) { if ($printQuery || $this->getFlag('print_query')) { - // phpcs:ignore Magento2.Security.LanguageConstruct + //phpcs:ignore Magento2.Security.LanguageConstruct echo $sql === null ? $this->getSelect()->__toString() : $sql; } @@ -828,7 +828,7 @@ public function __clone() * @return void * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ - protected function _initSelect() + protected function _initSelect() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { // no implementation, should be overridden in children classes } @@ -896,14 +896,9 @@ private function getMainTableAlias() /** * @inheritdoc * @since 100.0.11 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return array_diff( parent::__sleep(), ['_fetchStrategy', '_logger', '_conn', 'extensionAttributesJoinProcessor'] @@ -913,14 +908,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.11 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_logger = $objectManager->get(Logger::class); diff --git a/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php b/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php index b0f5742afef10..d8bb7a06e5b7d 100644 --- a/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php +++ b/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php @@ -57,17 +57,12 @@ public function __construct( } /** - * Remove links to other objects. + * Sleep magic method. * * @return array - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return ['_subject', '_isShared']; } @@ -75,14 +70,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } @@ -112,7 +102,7 @@ protected function _getSubject() } /** - * @inheritdoc + * @inheritDoc */ public function merge(array $config) { @@ -120,7 +110,7 @@ public function merge(array $config) } /** - * @inheritdoc + * @inheritDoc */ public function get($path = null, $default = null) { @@ -128,7 +118,7 @@ public function get($path = null, $default = null) } /** - * @inheritdoc + * @inheritDoc */ public function reset() { diff --git a/lib/internal/Magento/Framework/Encryption/Crypt.php b/lib/internal/Magento/Framework/Encryption/Crypt.php index d52db40b395ab..930cfa7a44f68 100644 --- a/lib/internal/Magento/Framework/Encryption/Crypt.php +++ b/lib/internal/Magento/Framework/Encryption/Crypt.php @@ -41,13 +41,13 @@ class Crypt /** * Constructor * - * @param string $key Secret encryption key. - * It's unsafe to store encryption key in memory, so no getter for key exists. - * @param string $cipher Cipher algorithm (one of the MCRYPT_ciphername constants) - * @param string $mode Mode of cipher algorithm (MCRYPT_MODE_modeabbr constants) - * @param string|bool $initVector Initial vector to fill algorithm blocks. - * TRUE generates a random initial vector. - * FALSE fills initial vector with zero bytes to not use it. + * @param string $key Secret encryption key. + * It's unsafe to store encryption key in memory, so no getter for key exists. + * @param string $cipher Cipher algorithm (one of the MCRYPT_ciphername constants) + * @param string $mode Mode of cipher algorithm (MCRYPT_MODE_modeabbr constants) + * @param string|bool $initVector Initial vector to fill algorithm blocks. + * TRUE generates a random initial vector. + * FALSE fills initial vector with zero bytes to not use it. * @throws \Exception */ public function __construct( @@ -66,7 +66,7 @@ public function __construct( $allowedCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $initVector = ''; for ($i = 0; $i < $initVectorSize; $i++) { - $initVector .= $allowedCharacters[rand(0, strlen($allowedCharacters) - 1)]; + $initVector .= $allowedCharacters[random_int(0, strlen($allowedCharacters) - 1)]; } // @codingStandardsIgnoreStart @mcrypt_generic_deinit($handle); diff --git a/lib/internal/Magento/Framework/Encryption/Encryptor.php b/lib/internal/Magento/Framework/Encryption/Encryptor.php index 791e6d72b951f..4bc1b2589362f 100644 --- a/lib/internal/Magento/Framework/Encryption/Encryptor.php +++ b/lib/internal/Magento/Framework/Encryption/Encryptor.php @@ -17,7 +17,9 @@ use Magento\Framework\Encryption\Adapter\Mcrypt; /** - * Class Encryptor provides basic logic for hashing strings and encrypting/decrypting misc data + * Class Encryptor provides basic logic for hashing strings and encrypting/decrypting misc data. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Encryptor implements EncryptorInterface { @@ -31,10 +33,17 @@ class Encryptor implements EncryptorInterface */ const HASH_VERSION_SHA256 = 1; + /** + * Key of Argon2ID13 algorithm + */ + public const HASH_VERSION_ARGON2ID13 = 2; + /** * Key of latest used algorithm + * @deprecated + * @see \Magento\Framework\Encryption\Encryptor::getLatestHashVersion */ - const HASH_VERSION_LATEST = 1; + const HASH_VERSION_LATEST = 2; /** * Default length of salt in bytes @@ -87,7 +96,7 @@ class Encryptor implements EncryptorInterface private $passwordHashMap = [ self::PASSWORD_HASH => '', self::PASSWORD_SALT => '', - self::PASSWORD_VERSION => self::HASH_VERSION_LATEST + self::PASSWORD_VERSION => self::HASH_VERSION_SHA256 ]; /** @@ -123,6 +132,7 @@ class Encryptor implements EncryptorInterface /** * Encryptor constructor. + * * @param Random $random * @param DeploymentConfig $deploymentConfig * @param KeyValidator|null $keyValidator @@ -138,6 +148,25 @@ public function __construct( $this->keys = preg_split('/\s+/s', trim((string)$deploymentConfig->get(self::PARAM_CRYPT_KEY))); $this->keyVersion = count($this->keys) - 1; $this->keyValidator = $keyValidator ?: ObjectManager::getInstance()->get(KeyValidator::class); + $latestHashVersion = $this->getLatestHashVersion(); + if ($latestHashVersion === self::HASH_VERSION_ARGON2ID13) { + $this->hashVersionMap[self::HASH_VERSION_ARGON2ID13] = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13; + $this->passwordHashMap[self::PASSWORD_VERSION] = self::HASH_VERSION_ARGON2ID13; + } + } + + /** + * Gets latest hash algorithm version. + * + * @return int + */ + public function getLatestHashVersion(): int + { + if (extension_loaded('sodium')) { + return self::HASH_VERSION_ARGON2ID13; + } + + return self::HASH_VERSION_SHA256; } /** @@ -160,6 +189,7 @@ public function validateCipher($version) $version = (int)$version; if (!in_array($version, $types, true)) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception((string)new \Magento\Framework\Phrase('Not supported cipher version')); } return $version; @@ -170,20 +200,34 @@ public function validateCipher($version) */ public function getHash($password, $salt = false, $version = self::HASH_VERSION_LATEST) { + if (!isset($this->hashVersionMap[$version])) { + $version = self::HASH_VERSION_SHA256; + } + if ($salt === false) { + $version = $version === self::HASH_VERSION_ARGON2ID13 ? self::HASH_VERSION_SHA256 : $version; return $this->hash($password, $version); } if ($salt === true) { $salt = self::DEFAULT_SALT_LENGTH; } if (is_integer($salt)) { + $salt = $version === self::HASH_VERSION_ARGON2ID13 ? + SODIUM_CRYPTO_PWHASH_SALTBYTES : + $salt; $salt = $this->random->getRandomString($salt); } + if ($version === self::HASH_VERSION_ARGON2ID13) { + $hash = $this->getArgonHash($password, $salt); + } else { + $hash = $this->generateSimpleHash($salt . $password, $version); + } + return implode( self::DELIMITER, [ - $this->hash($salt . $password, $version), + $hash, $salt, $version ] @@ -191,13 +235,29 @@ public function getHash($password, $salt = false, $version = self::HASH_VERSION_ } /** - * @inheritdoc + * Generate simple hash for given string. + * + * @param string $data + * @param int $version + * @return string */ - public function hash($data, $version = self::HASH_VERSION_LATEST) + private function generateSimpleHash(string $data, int $version): string { return hash($this->hashVersionMap[$version], (string)$data); } + /** + * @inheritdoc + */ + public function hash($data, $version = self::HASH_VERSION_SHA256) + { + if (empty($this->keys[$this->keyVersion])) { + throw new \RuntimeException('No key available'); + } + + return hash_hmac($this->hashVersionMap[$version], (string)$data, $this->keys[$this->keyVersion], false); + } + /** * @inheritdoc */ @@ -211,15 +271,24 @@ public function validateHash($password, $hash) */ public function isValidHash($password, $hash) { - $this->explodePasswordHash($hash); - - foreach ($this->getPasswordVersion() as $hashVersion) { - $password = $this->hash($this->getPasswordSalt() . $password, $hashVersion); + try { + $this->explodePasswordHash($hash); + foreach ($this->getPasswordVersion() as $hashVersion) { + if ($hashVersion === self::HASH_VERSION_ARGON2ID13) { + $recreated = $this->getArgonHash($password, $this->getPasswordSalt()); + } else { + $recreated = $this->generateSimpleHash($this->getPasswordSalt() . $password, $hashVersion); + } + $hash = $this->getPasswordHash(); + } + } catch (\RuntimeException $exception) { + //Hash is not a password hash. + $recreated = $this->hash($password); } return Security::compareStrings( - $password, - $this->getPasswordHash() + $recreated, + $hash ); } @@ -228,23 +297,32 @@ public function isValidHash($password, $hash) */ public function validateHashVersion($hash, $validateCount = false) { - $this->explodePasswordHash($hash); + try { + $this->explodePasswordHash($hash); + } catch (\RuntimeException $exception) { + //Not a password hash. + return true; + } $hashVersions = $this->getPasswordVersion(); return $validateCount - ? end($hashVersions) === self::HASH_VERSION_LATEST && count($hashVersions) === 1 - : end($hashVersions) === self::HASH_VERSION_LATEST; + ? end($hashVersions) === $this->getLatestHashVersion() && count($hashVersions) === 1 + : end($hashVersions) === $this->getLatestHashVersion(); } /** * Explode password hash * * @param string $hash + * @throws \RuntimeException When given hash cannot be processed. * @return array */ private function explodePasswordHash($hash) { $explodedPassword = explode(self::DELIMITER, $hash, 3); + if (count($explodedPassword) !== 3) { + throw new \RuntimeException('Hash is not a password hash'); + } foreach ($this->passwordHashMap as $key => $defaultValue) { $this->passwordHashMap[$key] = (isset($explodedPassword[$key])) ? $explodedPassword[$key] : $defaultValue; @@ -384,6 +462,7 @@ public function decrypt($data) public function validateKey($key) { if (!$this->keyValidator->isValid($key)) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( (string)new \Magento\Framework\Phrase( 'Encryption key must be 32 character string without any white space.' @@ -481,4 +560,32 @@ private function getCipherVersion() return self::CIPHER_RIJNDAEL_256; } } + + /** + * Generate Argon2ID13 hash. + * + * @param string $data + * @param string $salt + * @return string + * @throws \SodiumException + */ + private function getArgonHash($data, $salt = ''): string + { + $salt = empty($salt) ? + random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES) : + substr($salt, 0, SODIUM_CRYPTO_PWHASH_SALTBYTES); + + if (strlen($salt) < SODIUM_CRYPTO_PWHASH_SALTBYTES) { + $salt = str_pad($salt, SODIUM_CRYPTO_PWHASH_SALTBYTES, $salt); + } + + return bin2hex(sodium_crypto_pwhash( + SODIUM_CRYPTO_SIGN_SEEDBYTES, + $data, + $salt, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, + $this->hashVersionMap[self::HASH_VERSION_ARGON2ID13] + )); + } } diff --git a/lib/internal/Magento/Framework/Encryption/EncryptorInterface.php b/lib/internal/Magento/Framework/Encryption/EncryptorInterface.php index f7bc424a7a0d6..778cfcb897e0b 100644 --- a/lib/internal/Magento/Framework/Encryption/EncryptorInterface.php +++ b/lib/internal/Magento/Framework/Encryption/EncryptorInterface.php @@ -28,7 +28,9 @@ interface EncryptorInterface public function getHash($password, $salt = false); /** - * Hash a string + * Hash a string. + * + * Returns one-way encrypted string, always the same result for the same value. Suitable for signatures. * * @param string $data * @return string @@ -36,17 +38,20 @@ public function getHash($password, $salt = false); public function hash($data); /** - * Validate hash against hashing method (with or without salt) + * Synonym to isValidHash. * * @param string $password * @param string $hash * @return bool * @throws \Exception + * @see isValidHash */ public function validateHash($password, $hash); /** - * Validate hash against hashing method (with or without salt) + * Validate hash against hashing method. + * + * Works for both hashes returned by hash() and getHash(). * * @param string $password * @param string $hash diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php index 3feb4b4122843..602d7d5c59b95 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php @@ -16,9 +16,13 @@ use Magento\Framework\Encryption\KeyValidator; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Test case for \Magento\Framework\Encryption\Encryptor + */ class EncryptorTest extends \PHPUnit\Framework\TestCase { private const CRYPT_KEY_1 = 'g9mY9KLrcuAVJfsmVUSRkKFLDdUPVkaZ'; + private const CRYPT_KEY_2 = '7wEjmrliuqZQ1NQsndSa8C8WHvddeEbN'; /** @@ -27,15 +31,18 @@ class EncryptorTest extends \PHPUnit\Framework\TestCase private $encryptor; /** - * @var Random | \PHPUnit_Framework_MockObject_MockObject + * @var Random|\PHPUnit_Framework_MockObject_MockObject */ private $randomGeneratorMock; /** - * @var KeyValidator | \PHPUnit_Framework_MockObject_MockObject + * @var KeyValidator|\PHPUnit_Framework_MockObject_MockObject */ private $keyValidatorMock; + /** + * @inheritdoc + */ protected function setUp() { $this->randomGeneratorMock = $this->createMock(Random::class); @@ -56,48 +63,72 @@ protected function setUp() ); } + /** + * Hashing without a salt. + */ public function testGetHashNoSalt(): void { $this->randomGeneratorMock->expects($this->never())->method('getRandomString'); - $expected = '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'; + $expected = '1421feadb52d556a2045588672d8880d812ecc81ebb53dd98f6ff43500786b36'; $actual = $this->encryptor->getHash('password'); $this->assertEquals($expected, $actual); } + /** + * Providing salt for hash. + */ public function testGetHashSpecifiedSalt(): void { $this->randomGeneratorMock->expects($this->never())->method('getRandomString'); - $expected = '13601bda4ea78e55a07b98866d2be6be0744e3866f13c00c811cab608a28f322:salt:1'; + $expected = $this->encryptor->getLatestHashVersion() === Encryptor::HASH_VERSION_ARGON2ID13 ? + '7640855aef9cb6ffd20229601d2904a2192e372b391db8230d7faf073b393e4c:salt:2' : + '13601bda4ea78e55a07b98866d2be6be0744e3866f13c00c811cab608a28f322:salt:1'; $actual = $this->encryptor->getHash('password', 'salt'); $this->assertEquals($expected, $actual); } + /** + * Hashing with random salt. + */ public function testGetHashRandomSaltDefaultLength(): void { $salt = '-----------random_salt----------'; $this->randomGeneratorMock ->expects($this->once()) ->method('getRandomString') - ->with(32) + ->with($this->encryptor->getLatestHashVersion() === Encryptor::HASH_VERSION_ARGON2ID13 ? 16 : 32) ->willReturn($salt); - $expected = 'a1c7fc88037b70c9be84d3ad12522c7888f647915db78f42eb572008422ba2fa:' . $salt . ':1'; + $expected = $this->encryptor->getLatestHashVersion() === Encryptor::HASH_VERSION_ARGON2ID13 ? + '0be2351d7513d3e9622bd2df1891c39ba5ba6d1e3d67a058c60d6fd83f6641d8:' . $salt . ':2' : + 'a1c7fc88037b70c9be84d3ad12522c7888f647915db78f42eb572008422ba2fa:' . $salt . ':1'; $actual = $this->encryptor->getHash('password', true); $this->assertEquals($expected, $actual); } + /** + * Hashing with random salt of certain length. + */ public function testGetHashRandomSaltSpecifiedLength(): void { $this->randomGeneratorMock ->expects($this->once()) ->method('getRandomString') - ->with(11) - ->willReturn('random_salt'); - $expected = '4c5cab8dd00137d11258f8f87b93fd17bd94c5026fc52d3c5af911dd177a2611:random_salt:1'; + ->with($this->encryptor->getLatestHashVersion() === Encryptor::HASH_VERSION_ARGON2ID13 ? 16 : 11) + ->willReturn( + $this->encryptor->getLatestHashVersion() === Encryptor::HASH_VERSION_ARGON2ID13 ? + 'random_salt12345' : + 'random_salt' + ); + $expected = $this->encryptor->getLatestHashVersion() === Encryptor::HASH_VERSION_ARGON2ID13 ? + 'ca7982945fa90444b78d586678ff1c223ce13f99a39ec9541eae8b63ada3816a:random_salt12345:2' : + '4c5cab8dd00137d11258f8f87b93fd17bd94c5026fc52d3c5af911dd177a2611:random_salt:1'; $actual = $this->encryptor->getHash('password', 11); $this->assertEquals($expected, $actual); } /** + * Validating hashes generated by different algorithms. + * * @param string $password * @param string $hash * @param bool $expected @@ -111,6 +142,8 @@ public function testValidateHash($password, $hash, $expected): void } /** + * List of values and their hashes using different algorithms. + * * @return array */ public function validateHashDataProvider(): array @@ -123,9 +156,11 @@ public function validateHashDataProvider(): array } /** + * Encrypting with empty keys. + * * @param mixed $key * - * @dataProvider encryptWithEmptyKeyDataProvider + * @dataProvider emptyKeyDataProvider * @expectedException \SodiumException */ public function testEncryptWithEmptyKey($key): void @@ -141,17 +176,11 @@ public function testEncryptWithEmptyKey($key): void } /** - * @return array - */ - public function encryptWithEmptyKeyDataProvider(): array - { - return [[null], [0], [''], ['0']]; - } - - /** + * Seeing how decrypting works with invalid keys. + * * @param mixed $key * - * @dataProvider decryptWithEmptyKeyDataProvider + * @dataProvider emptyKeyDataProvider */ public function testDecryptWithEmptyKey($key): void { @@ -166,13 +195,18 @@ public function testDecryptWithEmptyKey($key): void } /** + * List of invalid keys. + * * @return array */ - public function decryptWithEmptyKeyDataProvider(): array + public function emptyKeyDataProvider(): array { return [[null], [0], [''], ['0']]; } + /** + * Seeing that encrypting uses sodium. + */ public function testEncrypt(): void { // sample data to encrypt @@ -181,13 +215,16 @@ public function testEncrypt(): void $actual = $this->encryptor->encrypt($data); // Extract the initialization vector and encrypted data - [, , $encryptedData] = explode(':', $actual, 3); + $encryptedParts = explode(':', $actual, 3); $crypt = new SodiumChachaIetf(self::CRYPT_KEY_1); // Verify decrypted matches original data - $this->assertEquals($data, $crypt->decrypt(base64_decode((string)$encryptedData))); + $this->assertEquals($data, $crypt->decrypt(base64_decode((string)$encryptedParts[2]))); } + /** + * Check that decrypting works. + */ public function testDecrypt(): void { $message = 'Mares eat oats and does eat oats, but little lambs eat ivy.'; @@ -196,6 +233,9 @@ public function testDecrypt(): void $this->assertEquals($message, $this->encryptor->decrypt($encrypted)); } + /** + * Using an old algo. + */ public function testLegacyDecrypt(): void { // sample data to encrypt @@ -213,6 +253,9 @@ public function testLegacyDecrypt(): void $this->assertEquals($encrypted, base64_encode($crypt->encrypt($actual))); } + /** + * Seeing that changing a key does not stand in a way of decrypting. + */ public function testEncryptDecryptNewKeyAdded(): void { $deploymentConfigMock = $this->createMock(DeploymentConfig::class); @@ -237,6 +280,9 @@ public function testEncryptDecryptNewKeyAdded(): void $this->assertSame($data, $decryptedData, 'Encryptor failed to decrypt data encrypted by old keys.'); } + /** + * Checking that encryptor relies on key validator. + */ public function testValidateKey(): void { $this->keyValidatorMock->method('isValid')->willReturn(true); @@ -244,6 +290,8 @@ public function testValidateKey(): void } /** + * Checking that encryptor relies on key validator. + * * @expectedException \Exception */ public function testValidateKeyInvalid(): void @@ -253,33 +301,75 @@ public function testValidateKeyInvalid(): void } /** + * Algorithms and expressions to validate them. + * * @return array */ public function useSpecifiedHashingAlgoDataProvider(): array { return [ - ['password', 'salt', Encryptor::HASH_VERSION_MD5, - '67a1e09bb1f83f5007dc119c14d663aa:salt:0'], - ['password', 'salt', Encryptor::HASH_VERSION_SHA256, - '13601bda4ea78e55a07b98866d2be6be0744e3866f13c00c811cab608a28f322:salt:1'], - ['password', false, Encryptor::HASH_VERSION_MD5, - '5f4dcc3b5aa765d61d8327deb882cf99'], - ['password', false, Encryptor::HASH_VERSION_SHA256, - '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'] + [ + 'password', + 'salt', + Encryptor::HASH_VERSION_MD5, + '/^[a-z0-9]{32}\:salt\:0$/' + ], + [ + 'password', + 'salt', + Encryptor::HASH_VERSION_SHA256, + '/^[a-z0-9]{64}\:salt\:1$/' + ], + [ + 'password', + false, + Encryptor::HASH_VERSION_MD5, + '/^[0-9a-z]{32}$/' + ], + [ + 'password', + false, + Encryptor::HASH_VERSION_SHA256, + '/^[0-9a-z]{64}$/' + ] ]; } /** + * Check that specified algorithm is in fact being used. + * * @dataProvider useSpecifiedHashingAlgoDataProvider * - * @param $password - * @param $salt - * @param $hashAlgo - * @param $expected + * @param string $password + * @param string|bool $salt + * @param int $hashAlgo + * @param string $pattern */ - public function testGetHashMustUseSpecifiedHashingAlgo($password, $salt, $hashAlgo, $expected): void + public function testGetHashMustUseSpecifiedHashingAlgo($password, $salt, $hashAlgo, $pattern): void { $hash = $this->encryptor->getHash($password, $salt, $hashAlgo); - $this->assertEquals($expected, $hash); + $this->assertRegExp($pattern, $hash); + } + + /** + * Test hashing working as promised. + */ + public function testHash() + { + //Checking that the same hash is returned for the same value. + $hash1 = $this->encryptor->hash($value = 'some value'); + $hash2 = $this->encryptor->hash($value); + $this->assertEquals($hash1, $hash2); + + //Checking that hash works with hash validation. + $this->assertTrue($this->encryptor->isValidHash($value, $hash1)); + + //Checking that key matters. + $this->keyValidatorMock->method('isValid')->willReturn(true); + $this->encryptor->setNewKey(self::CRYPT_KEY_2); + $hash3 = $this->encryptor->hash($value); + $this->assertNotEquals($hash3, $hash1); + //Validation still works + $this->assertTrue($this->encryptor->validateHash($value, $hash3)); } } diff --git a/lib/internal/Magento/Framework/Escaper.php b/lib/internal/Magento/Framework/Escaper.php index 216afc0409b50..0faf3bfa5e133 100644 --- a/lib/internal/Magento/Framework/Escaper.php +++ b/lib/internal/Magento/Framework/Escaper.php @@ -72,7 +72,7 @@ public function escapeHtml($data, $allowedTags = null) foreach ($data as $item) { $result[] = $this->escapeHtml($item, $allowedTags); } - } elseif (strlen($data)) { + } elseif (!empty($data)) { if (is_array($allowedTags) && !empty($allowedTags)) { $allowedTags = $this->filterProhibitedTags($allowedTags); $wrapperElementId = uniqid(); @@ -335,7 +335,8 @@ public function escapeXssInUrl($data) */ private function escapeScriptIdentifiers(string $data): string { - $filteredData = preg_replace(self::$xssFiltrationPattern, ':', $data) ?: ''; + $filteredData = preg_replace('/[\x00-\x1F\x7F\xA0]/u', '', $data) ?: ''; + $filteredData = preg_replace(self::$xssFiltrationPattern, ':', $filteredData) ?: ''; if (preg_match(self::$xssFiltrationPattern, $filteredData)) { $filteredData = $this->escapeScriptIdentifiers($filteredData); } diff --git a/lib/internal/Magento/Framework/Filter/Template.php b/lib/internal/Magento/Framework/Filter/Template.php index d3a8d5334ab9d..6a04e8e8c6953 100644 --- a/lib/internal/Magento/Framework/Filter/Template.php +++ b/lib/internal/Magento/Framework/Filter/Template.php @@ -75,7 +75,13 @@ class Template implements \Zend_Filter_Interface 'load', 'save', 'getcollection', - 'getresource' + 'getresource', + 'getconfig', + 'setvariables', + 'settemplateprocessor', + 'gettemplateprocessor', + 'vardirective', + 'delete' ]; /** diff --git a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php index e4a2dc48d11dd..0ee3a06ce5420 100644 --- a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php +++ b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php @@ -8,6 +8,9 @@ use Magento\Store\Model\Store; +/** + * Template Filter test. + */ class TemplateTest extends \PHPUnit\Framework\TestCase { /** @@ -420,8 +423,8 @@ public function testInappropriateCallbacks() */ public function testDisallowedMethods($method) { - $this->templateFilter->setVariables(['store' => $this->store]); - $this->templateFilter->filter('{{var store.'.$method.'()}}'); + $this->templateFilter->setVariables(['store' => $this->store, 'filter' => $this->templateFilter]); + $this->templateFilter->filter('{{var store.'.$method.'()}} {{var filter.' .$method .'()}}'); } /** @@ -437,6 +440,12 @@ public function disallowedMethods() ['save'], ['getCollection'], ['getResource'], + ['getConfig'], + ['setVariables'], + ['setTemplateProcessor'], + ['getTemplateProcessor'], + ['varDirective'], + ['delete'] ]; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/IdentityInterface.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/IdentityInterface.php index d65e86a37550d..943cd63417399 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/IdentityInterface.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/IdentityInterface.php @@ -7,11 +7,16 @@ namespace Magento\Framework\GraphQl\Query\Resolver; +/** + * IdentityInterface is responsible for generating the proper tags from a cache tag and resolved data. + */ interface IdentityInterface { /** - * Get identities from resolved data + * Get identity tags from resolved data. + * + * Example: identityTag, identityTag_UniqueId. * * @param array $resolvedData * @return string[] diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/CacheTagReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/CacheAnnotationReader.php similarity index 81% rename from lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/CacheTagReader.php rename to lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/CacheAnnotationReader.php index 2613b2829e79a..6cd822cd566ba 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/CacheTagReader.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/CacheAnnotationReader.php @@ -10,7 +10,7 @@ /** * Reads documentation from the annotation @cache of an AST node */ -class CacheTagReader +class CacheAnnotationReader { /** * Read documentation annotation for a specific node if exists @@ -24,12 +24,6 @@ public function read(\GraphQL\Language\AST\NodeList $directives) : array foreach ($directives as $directive) { if ($directive->name->value == 'cache') { foreach ($directive->arguments as $directiveArgument) { - if ($directiveArgument->name->value == 'cacheTag') { - $argMap = array_merge( - $argMap, - ["cacheTag" => $directiveArgument->value->value] - ); - } if ($directiveArgument->name->value == 'cacheable') { $argMap = array_merge( $argMap, diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php index 554d2636cf8c3..7438a4e3da932 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php @@ -23,24 +23,24 @@ class FieldMetaReader private $docReader; /** - * @var CacheTagReader + * @var CacheAnnotationReader */ - private $cacheTagReader; + private $cacheAnnotationReader; /** * @param TypeMetaWrapperReader $typeMetaReader * @param DocReader $docReader - * @param CacheTagReader|null $cacheTagReader + * @param CacheAnnotationReader|null $cacheAnnotationReader */ public function __construct( TypeMetaWrapperReader $typeMetaReader, DocReader $docReader, - CacheTagReader $cacheTagReader = null + CacheAnnotationReader $cacheAnnotationReader = null ) { $this->typeMetaReader = $typeMetaReader; $this->docReader = $docReader; - $this->cacheTagReader = $cacheTagReader ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(CacheTagReader::class); + $this->cacheAnnotationReader = $cacheAnnotationReader ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(CacheAnnotationReader::class); } /** @@ -73,7 +73,7 @@ public function read(\GraphQL\Type\Definition\FieldDefinition $fieldMeta) : arra } if ($this->docReader->read($fieldMeta->astNode->directives)) { - $result['cache'] = $this->cacheTagReader->read($fieldMeta->astNode->directives); + $result['cache'] = $this->cacheAnnotationReader->read($fieldMeta->astNode->directives); } $arguments = $fieldMeta->args; diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php index 2eda79ce68b04..38159fac03b3b 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeMetaReaderInterface; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\TypeMetaWrapperReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\DocReader; -use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheTagReader; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheAnnotationReader; /** * Composite configuration reader to handle the input object type meta @@ -28,24 +28,24 @@ class InputObjectType implements TypeMetaReaderInterface private $docReader; /** - * @var CacheTagReader + * @var CacheAnnotationReader */ - private $cacheTagReader; + private $cacheAnnotationReader; /** * @param TypeMetaWrapperReader $typeMetaReader * @param DocReader $docReader - * @param CacheTagReader|null $cacheTagReader + * @param CacheAnnotationReader|null $cacheAnnotationReader */ public function __construct( TypeMetaWrapperReader $typeMetaReader, DocReader $docReader, - CacheTagReader $cacheTagReader = null + CacheAnnotationReader $cacheAnnotationReader = null ) { $this->typeMetaReader = $typeMetaReader; $this->docReader = $docReader; - $this->cacheTagReader = $cacheTagReader ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(CacheTagReader::class); + $this->cacheAnnotationReader = $cacheAnnotationReader ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(CacheAnnotationReader::class); } /** @@ -70,7 +70,7 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array } if ($this->docReader->read($typeMeta->astNode->directives)) { - $result['cache'] = $this->cacheTagReader->read($typeMeta->astNode->directives); + $result['cache'] = $this->cacheAnnotationReader->read($typeMeta->astNode->directives); } return $result; } else { diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php index 7c040cd2e104c..baadb4be61cf2 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeMetaReaderInterface; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\FieldMetaReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\DocReader; -use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheTagReader; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheAnnotationReader; /** * Composite configuration reader to handle the interface object type meta @@ -28,24 +28,24 @@ class InterfaceType implements TypeMetaReaderInterface private $docReader; /** - * @var CacheTagReader + * @var CacheAnnotationReader */ - private $cacheTagReader; + private $cacheAnnotationReader; /** * @param FieldMetaReader $fieldMetaReader * @param DocReader $docReader - * @param CacheTagReader|null $cacheTagReader + * @param CacheAnnotationReader|null $cacheAnnotationReader */ public function __construct( FieldMetaReader $fieldMetaReader, DocReader $docReader, - CacheTagReader $cacheTagReader = null + CacheAnnotationReader $cacheAnnotationReader = null ) { $this->fieldMetaReader = $fieldMetaReader; $this->docReader = $docReader; - $this->cacheTagReader = $cacheTagReader ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(CacheTagReader::class); + $this->cacheAnnotationReader = $cacheAnnotationReader ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(CacheAnnotationReader::class); } /** @@ -76,7 +76,7 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array } if ($this->docReader->read($typeMeta->astNode->directives)) { - $result['cache'] = $this->cacheTagReader->read($typeMeta->astNode->directives); + $result['cache'] = $this->cacheAnnotationReader->read($typeMeta->astNode->directives); } return $result; diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php index 77a44460f00ae..7614c4954091d 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php @@ -11,7 +11,7 @@ use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\FieldMetaReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\DocReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\ImplementsReader; -use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheTagReader; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheAnnotationReader; /** * Composite configuration reader to handle the object type meta @@ -34,28 +34,28 @@ class ObjectType implements TypeMetaReaderInterface private $implementsAnnotation; /** - * @var CacheTagReader + * @var CacheAnnotationReader */ - private $cacheTagReader; + private $cacheAnnotationReader; /** * ObjectType constructor. * @param FieldMetaReader $fieldMetaReader * @param DocReader $docReader * @param ImplementsReader $implementsAnnotation - * @param CacheTagReader|null $cacheTagReader + * @param CacheAnnotationReader|null $cacheAnnotationReader */ public function __construct( FieldMetaReader $fieldMetaReader, DocReader $docReader, ImplementsReader $implementsAnnotation, - CacheTagReader $cacheTagReader = null + CacheAnnotationReader $cacheAnnotationReader = null ) { $this->fieldMetaReader = $fieldMetaReader; $this->docReader = $docReader; $this->implementsAnnotation = $implementsAnnotation; - $this->cacheTagReader = $cacheTagReader ?? \Magento\Framework\App\ObjectManager::getInstance() - ->get(CacheTagReader::class); + $this->cacheAnnotationReader = $cacheAnnotationReader ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(CacheAnnotationReader::class); } /** @@ -89,7 +89,7 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array } if ($this->docReader->read($typeMeta->astNode->directives)) { - $result['cache'] = $this->cacheTagReader->read($typeMeta->astNode->directives); + $result['cache'] = $this->cacheAnnotationReader->read($typeMeta->astNode->directives); } return $result; diff --git a/lib/internal/Magento/Framework/HTTP/Client/Curl.php b/lib/internal/Magento/Framework/HTTP/Client/Curl.php index 0ac65a420ddcf..8b90897481259 100644 --- a/lib/internal/Magento/Framework/HTTP/Client/Curl.php +++ b/lib/internal/Magento/Framework/HTTP/Client/Curl.php @@ -357,6 +357,7 @@ public function getStatus() protected function makeRequest($method, $uri, $params = []) { $this->_ch = curl_init(); + $this->curlOption(CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS | CURLPROTO_FTP | CURLPROTO_FTPS); $this->curlOption(CURLOPT_URL, $uri); if ($method == 'POST') { $this->curlOption(CURLOPT_POST, 1); diff --git a/lib/internal/Magento/Framework/HTTP/Test/Unit/Client/CurlTest.php b/lib/internal/Magento/Framework/HTTP/Test/Unit/Client/CurlTest.php new file mode 100644 index 0000000000000..128c4440063ec --- /dev/null +++ b/lib/internal/Magento/Framework/HTTP/Test/Unit/Client/CurlTest.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\HTTP\Test\Unit\Client; + +use Magento\Framework\HTTP\Client\Curl; +use PHPUnit\Framework\TestCase; + +/** + * Test HTTP client based on cUrl. + */ +class CurlTest extends TestCase +{ + /** + * Check that HTTP client can be used only for HTTP. + * + * @expectedException \Exception + * @expectedExceptionMessageRegExp /Protocol .?telnet.? not supported or disabled in libcurl/ + */ + public function testInvalidProtocol() + { + $client = new Curl(); + $client->get('telnet://127.0.0.1/test'); + } +} diff --git a/lib/internal/Magento/Framework/Interception/Interceptor.php b/lib/internal/Magento/Framework/Interception/Interceptor.php index df1b680234220..07600c5168181 100644 --- a/lib/internal/Magento/Framework/Interception/Interceptor.php +++ b/lib/internal/Magento/Framework/Interception/Interceptor.php @@ -62,14 +62,9 @@ public function ___callParent($method, array $arguments) * Calls parent class sleep if defined, otherwise provides own implementation * * @return array - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - if (method_exists(get_parent_class($this), '__sleep')) { $properties = parent::__sleep(); } else { @@ -83,14 +78,9 @@ public function __sleep() * Causes Interceptor to be initialized * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - if (method_exists(get_parent_class($this), '__wakeup')) { parent::__wakeup(); } diff --git a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php index e1f6c792c9c3e..949e002a14208 100644 --- a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php @@ -158,7 +158,7 @@ public function getCustomAttribute($attributeCode) } /** - * @inheritdoc + * @inheritDoc */ public function setCustomAttributes(array $attributes) { @@ -166,7 +166,7 @@ public function setCustomAttributes(array $attributes) } /** - * @inheritdoc + * @inheritDoc */ public function setCustomAttribute($attributeCode, $attributeValue) { @@ -182,9 +182,11 @@ public function setCustomAttribute($attributeCode, $attributeValue) } /** - * @inheritdoc + * {@inheritdoc} Added custom attributes support. * - * Added custom attributes support. + * @param string|array $key + * @param mixed $value + * @return $this */ public function setData($key, $value = null) { @@ -200,9 +202,10 @@ public function setData($key, $value = null) } /** - * @inheritdoc + * {@inheritdoc} Unset customAttributesChanged flag * - * Unset customAttributesChanged flag + * @param null|string|array $key + * @return $this */ public function unsetData($key = null) { @@ -359,27 +362,17 @@ private function populateExtensionAttributes(array $extensionAttributesData = [] /** * @inheritdoc - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return array_diff(parent::__sleep(), ['extensionAttributesFactory', 'customAttributeFactory']); } /** * @inheritdoc - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->extensionAttributesFactory = $objectManager->get(ExtensionAttributesFactory::class); diff --git a/lib/internal/Magento/Framework/Model/AbstractModel.php b/lib/internal/Magento/Framework/Model/AbstractModel.php index f5095dbb6e872..8018c6176390f 100644 --- a/lib/internal/Magento/Framework/Model/AbstractModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractModel.php @@ -10,6 +10,7 @@ /** * Abstract model class * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.NumberOfChildren) @@ -199,7 +200,7 @@ public function __construct( * * @return void */ - protected function _construct() + protected function _construct() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { } @@ -219,14 +220,9 @@ protected function _init($resourceModel) * Remove unneeded properties from serialization * * @return string[] - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $properties = array_keys(get_object_vars($this)); $properties = array_diff( $properties, @@ -248,14 +244,9 @@ public function __sleep() * Init not serializable fields * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_registry = $objectManager->get(\Magento\Framework\Registry::class); diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php index 3c88ee9dc5fed..fc0edf931fa9c 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php @@ -157,14 +157,9 @@ public function __construct( * Provide variables to serialize * * @return array - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $properties = array_keys(get_object_vars($this)); $properties = array_diff($properties, ['_resources', '_connections']); return $properties; @@ -174,14 +169,9 @@ public function __sleep() * Restore global dependencies * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->_resources = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\App\ResourceConnection::class); } @@ -230,10 +220,9 @@ protected function _setResource($connections, $tables = null) } /** - * Main table setter. + * Set main entity table name and primary key field name * - * Set main entity table name and primary key field name. - * If field name is omitted {table_name}_id will be used. + * If field name is omitted {table_name}_id will be used * * @param string $mainTable * @param string|null $idFieldName @@ -266,10 +255,7 @@ public function getIdFieldName() } /** - * Main table getter. - * - * Returns main table name - extracted from "module/table" style and - * validated by db adapter. + * Returns main table name - extracted from "module/table" style and validated by db adapter * * @throws LocalizedException * @return string @@ -550,6 +536,7 @@ public function getUniqueFields() * * @param \Magento\Framework\Model\AbstractModel $object * @return array + * @throws LocalizedException */ protected function _prepareDataForSave(\Magento\Framework\Model\AbstractModel $object) { @@ -557,10 +544,11 @@ protected function _prepareDataForSave(\Magento\Framework\Model\AbstractModel $o } /** - * Check that model data fields that can be saved has really changed comparing with origData. + * Check that model data fields that can be saved has really changed comparing with origData * * @param \Magento\Framework\Model\AbstractModel $object * @return bool + * @throws LocalizedException */ public function hasDataChanged($object) { @@ -742,6 +730,7 @@ public function getChecksum($table) * * @param \Magento\Framework\Model\AbstractModel $object * @return array + * @throws LocalizedException */ protected function prepareDataForUpdate($object) { diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php index bc2187f474919..cba5f133f53c8 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php @@ -12,6 +12,7 @@ /** * Abstract Resource Collection * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.NumberOfChildren) */ @@ -137,7 +138,7 @@ public function __construct( * * @return void */ - protected function _construct() + protected function _construct() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { } @@ -606,14 +607,9 @@ public function save() /** * @inheritdoc * @since 100.0.11 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return array_diff( parent::__sleep(), ['_resource', '_eventManager'] @@ -623,14 +619,9 @@ public function __sleep() /** * @inheritdoc * @since 100.0.11 - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_eventManager = $objectManager->get(\Magento\Framework\Event\ManagerInterface::class); diff --git a/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php b/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php index d67c380207554..470ba16bdd40c 100644 --- a/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php +++ b/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php @@ -55,17 +55,12 @@ public function __construct( } /** - * Remove links to objects. + * Sleep magic method. * * @return array - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return ['subject', 'isShared']; } @@ -73,14 +68,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } @@ -112,7 +102,7 @@ protected function _getSubject() } /** - * @inheritdoc + * @inheritDoc */ public function merge(array $config) { @@ -120,7 +110,7 @@ public function merge(array $config) } /** - * @inheritdoc + * @inheritDoc */ public function get($path = null, $default = null) { diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php index 2d585fe33aba3..aaf7d72db161a 100644 --- a/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php +++ b/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php @@ -237,7 +237,7 @@ public function testGetListWithException() $this->expectException('Exception'); $this->expectExceptionMessage("Table {$changelogTableName} does not exist"); $this->model->setViewId('viewIdtest'); - $this->model->getList(mt_rand(1, 200), mt_rand(201, 400)); + $this->model->getList(random_int(1, 200), random_int(201, 400)); } public function testClearWithException() @@ -249,7 +249,7 @@ public function testClearWithException() $this->expectException('Exception'); $this->expectExceptionMessage("Table {$changelogTableName} does not exist"); $this->model->setViewId('viewIdtest'); - $this->model->clear(mt_rand(1, 200)); + $this->model->clear(random_int(1, 200)); } /** diff --git a/lib/internal/Magento/Framework/Search/Request.php b/lib/internal/Magento/Framework/Search/Request.php index 60f3338046613..264d4929dde56 100644 --- a/lib/internal/Magento/Framework/Search/Request.php +++ b/lib/internal/Magento/Framework/Search/Request.php @@ -146,7 +146,13 @@ public function getSize() } /** - * @inheritdoc + * Temporary solution for an existing interface of a fulltext search request in Backward compatibility purposes. + * Don't use this function. + * It must be move to different interface. + * Scope to split Search request interface on two different 'Search' and 'Fulltext Search' contains in MC-16461. + * + * @deprecated + * @return array */ public function getSort() { diff --git a/lib/internal/Magento/Framework/Search/RequestInterface.php b/lib/internal/Magento/Framework/Search/RequestInterface.php index 2de756e754a27..16df80f755c07 100644 --- a/lib/internal/Magento/Framework/Search/RequestInterface.php +++ b/lib/internal/Magento/Framework/Search/RequestInterface.php @@ -64,11 +64,4 @@ public function getFrom(); * @return int|null */ public function getSize(); - - /** - * Get Sort items - * - * @return array - */ - public function getSort(); } diff --git a/lib/internal/Magento/Framework/Search/Response/QueryResponse.php b/lib/internal/Magento/Framework/Search/Response/QueryResponse.php index 90c7056ea2549..00b1ed2149bec 100644 --- a/lib/internal/Magento/Framework/Search/Response/QueryResponse.php +++ b/lib/internal/Magento/Framework/Search/Response/QueryResponse.php @@ -75,7 +75,14 @@ public function getAggregations() } /** - * @inheritdoc + * Temporary solution for an existing interface of a fulltext search request in Backward compatibility purposes. + * Don't use this function. + * It must be move to different interface. + * Scope to split Search response interface on two different 'Search' and 'Fulltext Search' contains in MC-16461. + * + * @deprecated + * + * @return int */ public function getTotal(): int { diff --git a/lib/internal/Magento/Framework/Search/ResponseInterface.php b/lib/internal/Magento/Framework/Search/ResponseInterface.php index c6c0d0ab59e10..3b89528532602 100644 --- a/lib/internal/Magento/Framework/Search/ResponseInterface.php +++ b/lib/internal/Magento/Framework/Search/ResponseInterface.php @@ -16,11 +16,4 @@ interface ResponseInterface extends \IteratorAggregate, \Countable * @return \Magento\Framework\Api\Search\AggregationInterface */ public function getAggregations(); - - /** - * Return total count of items. - * - * @return int - */ - public function getTotal(): int; } diff --git a/lib/internal/Magento/Framework/Search/Search.php b/lib/internal/Magento/Framework/Search/Search.php index fe228546b55fb..1286be59a0d8b 100644 --- a/lib/internal/Magento/Framework/Search/Search.php +++ b/lib/internal/Magento/Framework/Search/Search.php @@ -71,7 +71,16 @@ public function search(SearchCriteriaInterface $searchCriteria) $this->requestBuilder->setFrom($searchCriteria->getCurrentPage() * $searchCriteria->getPageSize()); $this->requestBuilder->setSize($searchCriteria->getPageSize()); - $this->requestBuilder->setSort($searchCriteria->getSortOrders()); + + /** + * This added in Backward compatibility purposes. + * Temporary solution for an existing API of a fulltext search request builder. + * It must be moved to different API. + * Scope to split Search request builder API in MC-16461. + */ + if (method_exists($this->requestBuilder, 'setSort')) { + $this->requestBuilder->setSort($searchCriteria->getSortOrders()); + } $request = $this->requestBuilder->create(); $searchResponse = $this->searchEngine->search($request); diff --git a/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php b/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php index 2314252f4609c..ca92ba69e5558 100644 --- a/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php +++ b/lib/internal/Magento/Framework/Search/SearchResponseBuilder.php @@ -51,7 +51,10 @@ public function build(ResponseInterface $response) $documents = iterator_to_array($response); $searchResult->setItems($documents); $searchResult->setAggregations($response->getAggregations()); - $searchResult->setTotalCount($response->getTotal()); + $count = method_exists($response, 'getTotal') + ? $response->getTotal() + : count($documents); + $searchResult->setTotalCount($count); return $searchResult; } diff --git a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php index e406994b54c17..7b45765fdefe8 100644 --- a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php @@ -43,6 +43,7 @@ protected function setUp() * * @param int $codepoint Unicode codepoint in hex notation * @return string UTF-8 literal string + * @throws \Exception */ protected function codepointToUtf8($codepoint) { @@ -265,15 +266,36 @@ public function escapeHtmlInvalidDataProvider() /** * @covers \Magento\Framework\Escaper::escapeUrl + * + * @param string $data + * @param string $expected + * @return void + * + * @dataProvider escapeUrlDataProvider */ - public function testEscapeUrl() + public function testEscapeUrl(string $data, string $expected): void { - $data = 'http://example.com/search?term=this+%26+that&view=list'; - $expected = 'http://example.com/search?term=this+%26+that&view=list'; $this->assertEquals($expected, $this->escaper->escapeUrl($data)); $this->assertEquals($expected, $this->escaper->escapeUrl($expected)); } + /** + * @return array + */ + public function escapeUrlDataProvider(): array + { + return [ + [ + 'data' => "http://example.com/search?term=this+%26+that&view=list", + 'expected' => "http://example.com/search?term=this+%26+that&view=list", + ], + [ + 'data' => "http://exam\r\nple.com/search?term=this+%26+that&view=list", + 'expected' => "http://example.com/search?term=this+%26+that&view=list", + ], + ]; + } + /** * @covers \Magento\Framework\Escaper::escapeJsQuote */ diff --git a/lib/internal/Magento/Framework/Translate/Inline/Proxy.php b/lib/internal/Magento/Framework/Translate/Inline/Proxy.php index e6d6cc57c2b09..d2b0468bebde9 100644 --- a/lib/internal/Magento/Framework/Translate/Inline/Proxy.php +++ b/lib/internal/Magento/Framework/Translate/Inline/Proxy.php @@ -55,17 +55,12 @@ public function __construct( } /** - * Remove links to other objects. + * Sleep magic method. * * @return array - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return ['subject', 'isShared']; } @@ -73,14 +68,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } @@ -132,7 +122,7 @@ public function getParser() /** * Replace translation templates with HTML fragments * - * @param array|string &$body + * @param array|string $body * @param bool $isJson * @return $this */ diff --git a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php index 43614e9cae83b..f8e8d2fee264a 100644 --- a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php +++ b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php @@ -8,7 +8,6 @@ use Magento\Framework\Cache\LockGuardedCacheLoader; use Magento\Framework\DataObject\IdentityInterface; -use Magento\Framework\App\ObjectManager; /** * Base class for all blocks. @@ -188,12 +187,10 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl * * @param \Magento\Framework\View\Element\Context $context * @param array $data - * @param LockGuardedCacheLoader|null $lockQuery */ public function __construct( \Magento\Framework\View\Element\Context $context, - array $data = [], - LockGuardedCacheLoader $lockQuery = null + array $data = [] ) { $this->_request = $context->getRequest(); $this->_layout = $context->getLayout(); @@ -212,12 +209,11 @@ public function __construct( $this->filterManager = $context->getFilterManager(); $this->_localeDate = $context->getLocaleDate(); $this->inlineTranslation = $context->getInlineTranslation(); + $this->lockQuery = $context->getLockGuardedCacheLoader(); if (isset($data['jsLayout'])) { $this->jsLayout = $data['jsLayout']; unset($data['jsLayout']); } - $this->lockQuery = $lockQuery - ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); parent::__construct($data); $this->_construct(); } diff --git a/lib/internal/Magento/Framework/View/Element/Context.php b/lib/internal/Magento/Framework/View/Element/Context.php index 522bec3e6a2a9..0f8123745e7e8 100644 --- a/lib/internal/Magento/Framework/View/Element/Context.php +++ b/lib/internal/Magento/Framework/View/Element/Context.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\View\Element; +use Magento\Framework\Cache\LockGuardedCacheLoader; +use Magento\Framework\App\ObjectManager; + /** * Constructor modification point for Magento\Framework\View\Element\AbstractBlock. * @@ -16,8 +19,7 @@ * As Magento moves from inheritance-based APIs all such classes will be deprecated together with * the classes they were introduced for. * - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD) * * @api */ @@ -136,12 +138,16 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface */ protected $inlineTranslation; + /** + * @var LockGuardedCacheLoader + */ + private $lockQuery; + /** * @param \Magento\Framework\App\RequestInterface $request * @param \Magento\Framework\View\LayoutInterface $layout * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\UrlInterface $urlBuilder - * @param \Magento\Framework\TranslateInterface $translator * @param \Magento\Framework\App\CacheInterface $cache * @param \Magento\Framework\View\DesignInterface $design * @param \Magento\Framework\Session\SessionManagerInterface $session @@ -155,6 +161,7 @@ class Context implements \Magento\Framework\ObjectManager\ContextInterface * @param \Magento\Framework\Filter\FilterManager $filterManager * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation + * @param LockGuardedCacheLoader $lockQuery * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -175,7 +182,8 @@ public function __construct( \Magento\Framework\Escaper $escaper, \Magento\Framework\Filter\FilterManager $filterManager, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation + \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, + LockGuardedCacheLoader $lockQuery = null ) { $this->_request = $request; $this->_layout = $layout; @@ -194,6 +202,7 @@ public function __construct( $this->_filterManager = $filterManager; $this->_localeDate = $localeDate; $this->inlineTranslation = $inlineTranslation; + $this->lockQuery = $lockQuery ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); } /** @@ -357,10 +366,22 @@ public function getFilterManager() } /** + * Get locale date. + * * @return \Magento\Framework\Stdlib\DateTime\TimezoneInterface */ public function getLocaleDate() { return $this->_localeDate; } + + /** + * Lock guarded cache loader. + * + * @return LockGuardedCacheLoader + */ + public function getLockGuardedCacheLoader() + { + return $this->lockQuery; + } } diff --git a/lib/internal/Magento/Framework/View/Element/Template/Context.php b/lib/internal/Magento/Framework/View/Element/Template/Context.php index f7f701b98f929..4538fb33a9726 100644 --- a/lib/internal/Magento/Framework/View/Element/Template/Context.php +++ b/lib/internal/Magento/Framework/View/Element/Template/Context.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\View\Element\Template; +use Magento\Framework\Cache\LockGuardedCacheLoader; + /** * Constructor modification point for Magento\Framework\View\Element\Template. * @@ -17,7 +19,7 @@ * the classes they were introduced for. * * @api - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD) */ class Context extends \Magento\Framework\View\Element\Context { @@ -105,6 +107,7 @@ class Context extends \Magento\Framework\View\Element\Context * @param \Magento\Framework\View\Page\Config $pageConfig * @param \Magento\Framework\View\Element\Template\File\Resolver $resolver * @param \Magento\Framework\View\Element\Template\File\Validator $validator + * @param LockGuardedCacheLoader|null $lockQuery * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -133,7 +136,8 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\View\Page\Config $pageConfig, \Magento\Framework\View\Element\Template\File\Resolver $resolver, - \Magento\Framework\View\Element\Template\File\Validator $validator + \Magento\Framework\View\Element\Template\File\Validator $validator, + LockGuardedCacheLoader $lockQuery = null ) { parent::__construct( $request, @@ -152,7 +156,8 @@ public function __construct( $escaper, $filterManager, $localeDate, - $inlineTranslation + $inlineTranslation, + $lockQuery ); $this->resolver = $resolver; $this->validator = $validator; @@ -246,6 +251,8 @@ public function getStoreManager() } /** + * Get page config. + * * @return \Magento\Framework\View\Page\Config */ public function getPageConfig() diff --git a/lib/internal/Magento/Framework/View/Layout/Proxy.php b/lib/internal/Magento/Framework/View/Layout/Proxy.php index a3d89c6ec7a8e..ec5ce761154ed 100644 --- a/lib/internal/Magento/Framework/View/Layout/Proxy.php +++ b/lib/internal/Magento/Framework/View/Layout/Proxy.php @@ -57,17 +57,12 @@ public function __construct( } /** - * Remove links to objects. + * Sleep magic method. * * @return array - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __sleep() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - return ['_subject', '_isShared']; } @@ -75,14 +70,9 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void - * - * @SuppressWarnings(PHPMD.SerializationAware) - * @deprecated Do not use PHP serialization. */ public function __wakeup() { - trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); - $this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } @@ -112,7 +102,7 @@ protected function _getSubject() } /** - * @inheritdoc + * @inheritDoc */ public function setGeneratorPool(\Magento\Framework\View\Layout\GeneratorPool $generatorPool) { @@ -120,7 +110,7 @@ public function setGeneratorPool(\Magento\Framework\View\Layout\GeneratorPool $g } /** - * @inheritdoc + * @inheritDoc */ public function setBuilder(\Magento\Framework\View\Layout\BuilderInterface $builder) { @@ -128,7 +118,7 @@ public function setBuilder(\Magento\Framework\View\Layout\BuilderInterface $buil } /** - * @inheritdoc + * @inheritDoc */ public function publicBuild() { @@ -136,7 +126,7 @@ public function publicBuild() } /** - * @inheritdoc + * @inheritDoc */ public function getUpdate() { @@ -144,7 +134,7 @@ public function getUpdate() } /** - * @inheritdoc + * @inheritDoc */ public function generateXml() { @@ -152,7 +142,7 @@ public function generateXml() } /** - * @inheritdoc + * @inheritDoc */ public function generateElements() { @@ -160,7 +150,7 @@ public function generateElements() } /** - * @inheritdoc + * @inheritDoc */ public function getChildBlock($parentName, $alias) { @@ -168,7 +158,7 @@ public function getChildBlock($parentName, $alias) } /** - * @inheritdoc + * @inheritDoc */ public function setChild($parentName, $elementName, $alias) { @@ -176,7 +166,7 @@ public function setChild($parentName, $elementName, $alias) } /** - * @inheritdoc + * @inheritDoc */ public function reorderChild($parentName, $childName, $offsetOrSibling, $after = true) { @@ -184,7 +174,7 @@ public function reorderChild($parentName, $childName, $offsetOrSibling, $after = } /** - * @inheritdoc + * @inheritDoc */ public function unsetChild($parentName, $alias) { @@ -192,7 +182,7 @@ public function unsetChild($parentName, $alias) } /** - * @inheritdoc + * @inheritDoc */ public function getChildNames($parentName) { @@ -200,7 +190,7 @@ public function getChildNames($parentName) } /** - * @inheritdoc + * @inheritDoc */ public function getChildBlocks($parentName) { @@ -208,7 +198,7 @@ public function getChildBlocks($parentName) } /** - * @inheritdoc + * @inheritDoc */ public function getChildName($parentName, $alias) { @@ -216,7 +206,7 @@ public function getChildName($parentName, $alias) } /** - * @inheritdoc + * @inheritDoc */ public function renderElement($name, $useCache = true) { @@ -224,7 +214,7 @@ public function renderElement($name, $useCache = true) } /** - * @inheritdoc + * @inheritDoc */ public function renderNonCachedElement($name) { @@ -232,7 +222,7 @@ public function renderNonCachedElement($name) } /** - * @inheritdoc + * @inheritDoc */ public function addToParentGroup($blockName, $parentGroupName) { @@ -240,7 +230,7 @@ public function addToParentGroup($blockName, $parentGroupName) } /** - * @inheritdoc + * @inheritDoc */ public function getGroupChildNames($blockName, $groupName) { @@ -248,7 +238,7 @@ public function getGroupChildNames($blockName, $groupName) } /** - * @inheritdoc + * @inheritDoc */ public function hasElement($name) { @@ -256,7 +246,7 @@ public function hasElement($name) } /** - * @inheritdoc + * @inheritDoc */ public function getElementProperty($name, $attribute) { @@ -264,7 +254,7 @@ public function getElementProperty($name, $attribute) } /** - * @inheritdoc + * @inheritDoc */ public function isBlock($name) { @@ -272,7 +262,7 @@ public function isBlock($name) } /** - * @inheritdoc + * @inheritDoc */ public function isUiComponent($name) { @@ -280,7 +270,7 @@ public function isUiComponent($name) } /** - * @inheritdoc + * @inheritDoc */ public function isContainer($name) { @@ -288,7 +278,7 @@ public function isContainer($name) } /** - * @inheritdoc + * @inheritDoc */ public function isManipulationAllowed($name) { @@ -296,7 +286,7 @@ public function isManipulationAllowed($name) } /** - * @inheritdoc + * @inheritDoc */ public function setBlock($name, $block) { @@ -304,7 +294,7 @@ public function setBlock($name, $block) } /** - * @inheritdoc + * @inheritDoc */ public function unsetElement($name) { @@ -312,7 +302,7 @@ public function unsetElement($name) } /** - * @inheritdoc + * @inheritDoc */ public function createBlock($type, $name = '', array $arguments = []) { @@ -320,7 +310,7 @@ public function createBlock($type, $name = '', array $arguments = []) } /** - * @inheritdoc + * @inheritDoc */ public function addBlock($block, $name = '', $parent = '', $alias = '') { @@ -328,7 +318,7 @@ public function addBlock($block, $name = '', $parent = '', $alias = '') } /** - * @inheritdoc + * @inheritDoc */ public function addContainer($name, $label, array $options = [], $parent = '', $alias = '') { @@ -336,7 +326,7 @@ public function addContainer($name, $label, array $options = [], $parent = '', $ } /** - * @inheritdoc + * @inheritDoc */ public function renameElement($oldName, $newName) { @@ -344,7 +334,7 @@ public function renameElement($oldName, $newName) } /** - * @inheritdoc + * @inheritDoc */ public function getAllBlocks() { @@ -352,7 +342,7 @@ public function getAllBlocks() } /** - * @inheritdoc + * @inheritDoc */ public function getBlock($name) { @@ -360,7 +350,7 @@ public function getBlock($name) } /** - * @inheritdoc + * @inheritDoc */ public function getUiComponent($name) { @@ -368,7 +358,7 @@ public function getUiComponent($name) } /** - * @inheritdoc + * @inheritDoc */ public function getParentName($childName) { @@ -376,7 +366,7 @@ public function getParentName($childName) } /** - * @inheritdoc + * @inheritDoc */ public function getElementAlias($name) { @@ -384,7 +374,7 @@ public function getElementAlias($name) } /** - * @inheritdoc + * @inheritDoc */ public function addOutputElement($name) { @@ -392,7 +382,7 @@ public function addOutputElement($name) } /** - * @inheritdoc + * @inheritDoc */ public function removeOutputElement($name) { @@ -400,7 +390,7 @@ public function removeOutputElement($name) } /** - * @inheritdoc + * @inheritDoc */ public function getOutput() { @@ -408,7 +398,7 @@ public function getOutput() } /** - * @inheritdoc + * @inheritDoc */ public function getMessagesBlock() { @@ -416,7 +406,7 @@ public function getMessagesBlock() } /** - * @inheritdoc + * @inheritDoc */ public function getBlockSingleton($type) { @@ -424,7 +414,7 @@ public function getBlockSingleton($type) } /** - * @inheritdoc + * @inheritDoc */ public function addAdjustableRenderer($namespace, $staticType, $dynamicType, $type, $template, $data = []) { @@ -439,7 +429,7 @@ public function addAdjustableRenderer($namespace, $staticType, $dynamicType, $ty } /** - * @inheritdoc + * @inheritDoc */ public function getRendererOptions($namespace, $staticType, $dynamicType) { @@ -447,7 +437,7 @@ public function getRendererOptions($namespace, $staticType, $dynamicType) } /** - * @inheritdoc + * @inheritDoc */ public function executeRenderer($namespace, $staticType, $dynamicType, $data = []) { @@ -455,7 +445,7 @@ public function executeRenderer($namespace, $staticType, $dynamicType, $data = [ } /** - * @inheritdoc + * @inheritDoc */ public function initMessages($messageGroups = []) { @@ -463,7 +453,7 @@ public function initMessages($messageGroups = []) } /** - * @inheritdoc + * @inheritDoc */ public function isCacheable() { @@ -471,7 +461,7 @@ public function isCacheable() } /** - * @inheritdoc + * @inheritDoc */ public function isPrivate() { @@ -479,7 +469,7 @@ public function isPrivate() } /** - * @inheritdoc + * @inheritDoc */ public function setIsPrivate($isPrivate = true) { @@ -487,7 +477,7 @@ public function setIsPrivate($isPrivate = true) } /** - * @inheritdoc + * @inheritDoc */ public function getReaderContext() { @@ -495,7 +485,7 @@ public function getReaderContext() } /** - * @inheritdoc + * @inheritDoc */ public function setXml(\Magento\Framework\Simplexml\Element $node) { @@ -503,7 +493,7 @@ public function setXml(\Magento\Framework\Simplexml\Element $node) } /** - * @inheritdoc + * @inheritDoc */ public function getNode($path = null) { @@ -511,7 +501,7 @@ public function getNode($path = null) } /** - * @inheritdoc + * @inheritDoc */ public function getXpath($xpath) { @@ -519,7 +509,7 @@ public function getXpath($xpath) } /** - * @inheritdoc + * @inheritDoc */ public function getXmlString() { @@ -527,7 +517,7 @@ public function getXmlString() } /** - * @inheritdoc + * @inheritDoc */ public function loadFile($filePath) { @@ -535,7 +525,7 @@ public function loadFile($filePath) } /** - * @inheritdoc + * @inheritDoc */ public function loadString($string) { @@ -543,7 +533,7 @@ public function loadString($string) } /** - * @inheritdoc + * @inheritDoc */ public function loadDom(\DOMNode $dom) { @@ -551,7 +541,7 @@ public function loadDom(\DOMNode $dom) } /** - * @inheritdoc + * @inheritDoc */ public function setNode($path, $value, $overwrite = true) { @@ -559,7 +549,7 @@ public function setNode($path, $value, $overwrite = true) } /** - * @inheritdoc + * @inheritDoc */ public function applyExtends() { @@ -567,7 +557,7 @@ public function applyExtends() } /** - * @inheritdoc + * @inheritDoc */ public function processFileData($text) { @@ -575,7 +565,7 @@ public function processFileData($text) } /** - * @inheritdoc + * @inheritDoc */ public function extend(\Magento\Framework\Simplexml\Config $config, $overwrite = true) { diff --git a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php index a0cdbfb7d8fe7..3ccc144ebecd5 100644 --- a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php +++ b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php @@ -5,10 +5,12 @@ */ namespace Magento\Framework\View\Model\Layout; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\State; use Magento\Framework\Config\Dom\ValidationException; use Magento\Framework\Filesystem\DriverPool; use Magento\Framework\Filesystem\File\ReadFactory; +use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; use Magento\Framework\View\Model\Layout\Update\Validator; @@ -42,7 +44,7 @@ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface /** * Cache id suffix for page layout */ - const PAGE_LAYOUT_CACHE_SUFFIX = 'page_layout'; + const PAGE_LAYOUT_CACHE_SUFFIX = 'page_layout_merged'; /** * @var \Magento\Framework\View\Design\ThemeInterface @@ -54,6 +56,11 @@ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface */ private $scope; + /** + * @var SerializerInterface + */ + private $serializer; + /** * In-memory cache for loaded layout updates * @@ -173,10 +180,11 @@ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface * @param \Magento\Framework\Cache\FrontendInterface $cache * @param \Magento\Framework\View\Model\Layout\Update\Validator $validator * @param \Psr\Log\LoggerInterface $logger - * @param ReadFactory $readFactory , + * @param ReadFactory $readFactory * @param \Magento\Framework\View\Design\ThemeInterface $theme Non-injectable theme instance * @param string $cacheSuffix * @param LayoutCacheKeyInterface $layoutCacheKey + * @param SerializerInterface|null $serializer * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -191,7 +199,8 @@ public function __construct( ReadFactory $readFactory, \Magento\Framework\View\Design\ThemeInterface $theme = null, $cacheSuffix = '', - LayoutCacheKeyInterface $layoutCacheKey = null + LayoutCacheKeyInterface $layoutCacheKey = null, + SerializerInterface $serializer = null ) { $this->theme = $theme ?: $design->getDesignTheme(); $this->scope = $scopeResolver->getScope(); @@ -205,6 +214,7 @@ public function __construct( $this->cacheSuffix = $cacheSuffix; $this->layoutCacheKey = $layoutCacheKey ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class); + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); } /** @@ -283,6 +293,7 @@ public function getHandles() /** * Add the first existing (declared in layout updates) page handle along with all parents to the update. + * * Return whether any page handles have been added or not. * * @param string[] $handlesToTry @@ -315,6 +326,8 @@ public function pageHandleExists($handleName) } /** + * Page layout type + * * @return string|null */ public function getPageLayout() @@ -437,12 +450,12 @@ public function load($handles = []) $this->addHandle($handles); - $cacheId = $this->getCacheId(); - $cacheIdPageLayout = $cacheId . '_' . self::PAGE_LAYOUT_CACHE_SUFFIX; + $cacheId = $this->getCacheId() . '_' . self::PAGE_LAYOUT_CACHE_SUFFIX; $result = $this->_loadCache($cacheId); - if ($result) { - $this->addUpdate($result); - $this->pageLayout = $this->_loadCache($cacheIdPageLayout); + if ($result !== false && $result !== null) { + $data = $this->serializer->unserialize($result); + $this->pageLayout = $data["pageLayout"]; + $this->addUpdate($data["layout"]); foreach ($this->getHandles() as $handle) { $this->allHandles[$handle] = $this->handleProcessed; } @@ -455,8 +468,13 @@ public function load($handles = []) $layout = $this->asString(); $this->_validateMergedLayout($cacheId, $layout); - $this->_saveCache($layout, $cacheId, $this->getHandles()); - $this->_saveCache((string)$this->pageLayout, $cacheIdPageLayout, $this->getHandles()); + + $data = [ + "pageLayout" => (string)$this->pageLayout, + "layout" => $layout + ]; + $this->_saveCache($this->serializer->serialize($data), $cacheId, $this->getHandles()); + return $this; } @@ -602,7 +620,7 @@ protected function _fetchDbLayoutUpdates($handle) */ public function validateUpdate($handle, $updateXml) { - return; + return null; } /** @@ -930,6 +948,7 @@ public function getScope() public function getCacheId() { $layoutCacheKeys = $this->layoutCacheKey->getCacheKeys(); + // phpcs:ignore Magento2.Security.InsecureFunction return $this->generateCacheId(md5(implode('|', array_merge($this->getHandles(), $layoutCacheKeys)))); } } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php index fbaa7ec670794..93192201c7831 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/AbstractBlockTest.php @@ -99,12 +99,14 @@ protected function setUp() $contextMock->expects($this->once()) ->method('getEscaper') ->willReturn($this->escaperMock); + $contextMock->expects($this->once()) + ->method('getLockGuardedCacheLoader') + ->willReturn($this->lockQuery); $this->block = $this->getMockForAbstractClass( AbstractBlock::class, [ 'context' => $contextMock, 'data' => [], - 'lockQuery' => $this->lockQuery ] ); } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php index 112d171f2574b..3060ac64d74bf 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php @@ -11,6 +11,11 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; +/** + * Class MergeTest + * + * @package Magento\Framework\View\Test\Unit\Model\Layout + */ class MergeTest extends \PHPUnit\Framework\TestCase { /** @@ -28,11 +33,21 @@ class MergeTest extends \PHPUnit\Framework\TestCase */ private $scope; + /** + * @var \Magento\Framework\Cache\FrontendInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $cache; + /** * @var \Magento\Framework\View\Model\Layout\Update\Validator|\PHPUnit_Framework_MockObject_MockObject */ private $layoutValidator; + /** + * @var \Magento\Framework\Serialize\SerializerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $serializer; + /** * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -53,10 +68,12 @@ protected function setUp() $this->objectManagerHelper = new ObjectManager($this); $this->scope = $this->getMockForAbstractClass(\Magento\Framework\Url\ScopeInterface::class); + $this->cache = $this->getMockForAbstractClass(\Magento\Framework\Cache\FrontendInterface::class); $this->layoutValidator = $this->getMockBuilder(\Magento\Framework\View\Model\Layout\Update\Validator::class) ->disableOriginalConstructor() ->getMock(); $this->logger = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class); + $this->serializer = $this->getMockForAbstractClass(\Magento\Framework\Serialize\SerializerInterface::class); $this->appState = $this->getMockBuilder(\Magento\Framework\App\State::class) ->disableOriginalConstructor() ->getMock(); @@ -70,10 +87,12 @@ protected function setUp() \Magento\Framework\View\Model\Layout\Merge::class, [ 'scope' => $this->scope, + 'cache' => $this->cache, 'layoutValidator' => $this->layoutValidator, 'logger' => $this->logger, 'appState' => $this->appState, 'layoutCacheKey' => $this->layoutCacheKeyMock, + 'serializer' => $this->serializer, ] ); } @@ -104,4 +123,33 @@ public function testValidateMergedLayoutThrowsException() $this->model->load(); } + + /** + * Test that merged layout is saved to cache if it wasn't cached before. + */ + public function testSaveToCache() + { + $this->scope->expects($this->once())->method('getId')->willReturn(1); + $this->cache->expects($this->once())->method('save'); + + $this->model->load(); + } + + /** + * Test that merged layout is not re-saved to cache when it was loaded from cache. + */ + public function testNoSaveToCacheWhenCachePresent() + { + $cacheValue = [ + "pageLayout" => "1column", + "layout" => "<body></body>" + ]; + + $this->scope->expects($this->once())->method('getId')->willReturn(1); + $this->cache->expects($this->once())->method('load')->willReturn(json_encode($cacheValue)); + $this->serializer->expects($this->once())->method('unserialize')->willReturn($cacheValue); + $this->cache->expects($this->never())->method('save'); + + $this->model->load(); + } } diff --git a/lib/web/mage/storage.js b/lib/web/mage/storage.js index 4df3ae755ec16..1e136aa78477b 100644 --- a/lib/web/mage/storage.js +++ b/lib/web/mage/storage.js @@ -55,17 +55,22 @@ define(['jquery', 'mage/url'], function ($, urlBuilder) { * @param {String} contentType * @returns {Deferred} */ - put: function (url, data, global, contentType) { + put: function (url, data, global, contentType, headers) { + var ajaxSettings = {}; + global = global === undefined ? true : global; contentType = contentType || 'application/json'; + ajaxSettings.url = urlBuilder.build(url); + ajaxSettings.type = 'PUT'; + ajaxSettings.data = data; + ajaxSettings.global = global; + ajaxSettings.contentType = contentType; - return $.ajax({ - url: urlBuilder.build(url), - type: 'PUT', - data: data, - global: global, - contentType: contentType - }); + if (headers) { + ajaxSettings.headers = headers; + } + + return $.ajax(ajaxSettings); }, /** diff --git a/lib/web/mage/utils/template.js b/lib/web/mage/utils/template.js index 7c50226d6aa3a..7aa695023cb56 100644 --- a/lib/web/mage/utils/template.js +++ b/lib/web/mage/utils/template.js @@ -32,6 +32,35 @@ define([ } })(); + /** + * Validates template + * + * @param {String} tmpl + * @param {Object} target + * @returns {Boolean} + */ + function isTmplIgnored(tmpl, target) { + var parsedTmpl; + + try { + parsedTmpl = JSON.parse(tmpl); + + if (typeof parsedTmpl === 'object') { + return tmpl.includes('__disableTmpl'); + } + } catch (e) { + } + + if (typeof target !== 'undefined') { + if (typeof target === 'object' && target.hasOwnProperty('__disableTmpl')) { + return true; + } + } + + return false; + + } + if (hasStringTmpls) { /*eslint-disable no-unused-vars, no-eval*/ @@ -40,10 +69,16 @@ define([ * * @param {String} tmpl - Template string. * @param {Object} $ - Data object used in a template. + * @param {Object} target * @returns {String} Compiled template. */ - template = function (tmpl, $) { - return eval('`' + tmpl + '`'); + template = function (tmpl, $, target) { + + if (!isTmplIgnored(tmpl, target)) { + return eval('`' + tmpl + '`'); + } + + return tmpl; }; /*eslint-enable no-unused-vars, no-eval*/ @@ -97,11 +132,11 @@ define([ * that it should be leaved as a string. * @returns {*} Compiled template. */ - function render(tmpl, data, castString) { + function render(tmpl, data, castString, target) { var last = tmpl; while (~tmpl.indexOf(opener)) { - tmpl = template(tmpl, data); + tmpl = template(tmpl, data, target); if (tmpl === last) { break; @@ -178,7 +213,7 @@ define([ } if (isTemplate(value)) { - list[key] = render(value, tmpl, castString); + list[key] = render(value, tmpl, castString, list); } else if ($.isPlainObject(value) || Array.isArray(value)) { _.each(value, iterate); } diff --git a/setup/src/Magento/Setup/Fixtures/BundleProductsFixture.php b/setup/src/Magento/Setup/Fixtures/BundleProductsFixture.php index 99c25089e68bb..3ad22e8f7bcd1 100644 --- a/setup/src/Magento/Setup/Fixtures/BundleProductsFixture.php +++ b/setup/src/Magento/Setup/Fixtures/BundleProductsFixture.php @@ -99,7 +99,8 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ public function execute() @@ -164,6 +165,7 @@ public function execute() 'sku' => $skuClosure, 'meta_title' => $skuClosure, 'price' => function ($index) use ($priceTypeClosure) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction return $priceTypeClosure($index) === LinkInterface::PRICE_TYPE_PERCENT ? mt_rand(10, 90) : $this->priceProvider->getPrice($index); @@ -242,7 +244,7 @@ private function getBundleVariationIndex($entityNumber, $variationCount) } /** - * {@inheritdoc} + * @inheritdoc */ public function getActionTitle() { @@ -250,7 +252,7 @@ public function getActionTitle() } /** - * {@inheritdoc} + * @inheritdoc */ public function introduceParamLabels() { diff --git a/setup/src/Magento/Setup/Fixtures/ConfigurableProductsFixture.php b/setup/src/Magento/Setup/Fixtures/ConfigurableProductsFixture.php index 00c60a2a8a6a5..8c2aed521dbe8 100644 --- a/setup/src/Magento/Setup/Fixtures/ConfigurableProductsFixture.php +++ b/setup/src/Magento/Setup/Fixtures/ConfigurableProductsFixture.php @@ -203,7 +203,8 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD) */ public function execute() @@ -296,6 +297,8 @@ public function execute() } /** + * Get the closure to return the website IDs. + * * @return \Closure * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -317,8 +320,10 @@ private function getDefaultAttributeSetsConfig(array $defaultAttributeSets, $con { $attributeSetClosure = function ($index) use ($defaultAttributeSets) { $attributeSetAmount = count(array_keys($defaultAttributeSets)); + // phpcs:ignore mt_srand($index); + // phpcs:ignore Magento2.Functions.DiscouragedFunction return $attributeSetAmount > ($index - 1) % (int)$this->fixtureModel->getValue('categories', 30) ? array_keys($defaultAttributeSets)[mt_rand(0, $attributeSetAmount - 1)] : 'Default'; @@ -399,7 +404,7 @@ private function getConfigurableVariationIndex($entityNumber, $variationCount) } /** - * {@inheritdoc} + * @inheritdoc */ public function getActionTitle() { @@ -407,7 +412,7 @@ public function getActionTitle() } /** - * {@inheritdoc} + * @inheritdoc */ public function introduceParamLabels() { @@ -415,7 +420,10 @@ public function introduceParamLabels() } /** - * {@inheritdoc} + * @inheritdoc + * + * @param OutputInterface $output + * @return void * @throws ValidatorException */ public function printInfo(OutputInterface $output) @@ -433,7 +441,8 @@ public function printInfo(OutputInterface $output) } /** - * Gen default attribute sets with attributes + * Get default attribute sets with attributes. + * * @see config/attributeSets.xml * * @return array @@ -560,8 +569,10 @@ private function getConfigurableProductConfig() } /** - * Prepare configuration. If amount of configurable products set in profile then return predefined attribute sets - * else return configuration from profile + * Prepare configuration. + * + * If amount of configurable products set in profile then return predefined attribute sets + * else return configuration from profile. * * @param array $defaultAttributeSets * @return array @@ -600,6 +611,8 @@ private function prepareConfigurableConfig($defaultAttributeSets) } /** + * Get closure to return configurable category. + * * @param array $config * @return \Closure */ @@ -623,6 +636,8 @@ private function getConfigurableCategory($config) } /** + * Get sku pattern. + * * @param array $config * @param string $attributeSetName * @return string @@ -693,6 +708,8 @@ function ($index, $attribute) use ($attributeSetName, $attributes, $attributeSet } /** + * Get search configuration. + * * @return array */ private function getSearchConfig() @@ -704,6 +721,8 @@ private function getSearchConfig() } /** + * Get value of search configuration property. + * * @param string $name * @return int|mixed */ @@ -714,6 +733,8 @@ private function getSearchConfigValue($name) } /** + * Get search terms. + * * @return array */ private function getSearchTerms() @@ -771,6 +792,7 @@ private function getAdditionalAttributesClosure(array $attributes, $variationCou /** * Generates matrix of all possible variations. + * * @param int $attributesPerSet * @param int $optionsPerAttribute * @return array @@ -786,6 +808,7 @@ private function generateVariationsMatrix($attributesPerSet, $optionsPerAttribut /** * Build all possible variations based on attributes and options count. + * * @param array|null $variationsMatrix * @return array */ @@ -818,6 +841,8 @@ private function getConfigurableOptionSkuPattern($skuPattern) } /** + * Get description closure. + * * @param array|null $searchTerms * @param int $simpleProductsCount * @param int $configurableProductsCount @@ -836,7 +861,7 @@ private function getDescriptionClosure( ) { if (null === $this->dataGenerator) { $fileName = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'dictionary.csv'; - $this->dataGenerator = new DataGenerator(realpath($fileName)); + $this->dataGenerator = new DataGenerator($fileName); } return function ($index) use ( @@ -855,6 +880,7 @@ private function getDescriptionClosure( $configurableProductsCount / ($simpleProductsCount + $configurableProductsCount) ) ); + // phpcs:ignore mt_srand($index); return $this->dataGenerator->generate( $minAmountOfWordsDescription, diff --git a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php index b5ea18f9cee2b..cfcdebd4ac373 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php @@ -42,6 +42,7 @@ public function __construct( */ public function generate($config) { + // phpcs:disable Magento2.Functions.DiscouragedFunction $binaryData = ''; $data = str_split(sha1($config['image-name']), 2); foreach ($data as $item) { @@ -72,6 +73,7 @@ public function generate($config) $absolutePathToMedia = $mediaDirectory->getAbsolutePath($this->mediaConfig->getBaseTmpMediaPath()); $imagePath = $absolutePathToMedia . DIRECTORY_SEPARATOR . $config['image-name']; imagejpeg($image, $imagePath, 100); + // phpcs:enable return $imagePath; } diff --git a/setup/src/Magento/Setup/Fixtures/OrdersFixture.php b/setup/src/Magento/Setup/Fixtures/OrdersFixture.php index 9fbec3b3741b2..936a82bebf246 100644 --- a/setup/src/Magento/Setup/Fixtures/OrdersFixture.php +++ b/setup/src/Magento/Setup/Fixtures/OrdersFixture.php @@ -11,6 +11,7 @@ /** * Fixture generator for Order entities with configurable number of different types of order items. + * * Optionally generates inactive quotes for generated orders. * * Support the following format: @@ -323,6 +324,7 @@ public function execute() $entityId++; while ($entityId <= $requestedOrders) { $batchNumber++; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $productCount = [ Type::TYPE_SIMPLE => mt_rand($orderSimpleCountFrom, $orderSimpleCountTo), Configurable::TYPE_CODE => mt_rand($orderConfigurableCountFrom, $orderConfigurableCountTo), @@ -473,6 +475,7 @@ public function execute() private function prepareQueryTemplates() { $fileName = __DIR__ . DIRECTORY_SEPARATOR . "_files" . DIRECTORY_SEPARATOR . "orders_fixture_data.json"; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $templateData = json_decode(file_get_contents(realpath($fileName)), true); foreach ($templateData as $table => $template) { if (isset($template['_table'])) { @@ -511,6 +514,7 @@ private function prepareQueryTemplates() $connection->beginTransaction(); } + // phpcs:ignore Magento2.SQL.RawQuery $this->queryTemplates[$table] = "INSERT INTO `{$tableName}` ({$fields}) VALUES ({$values}){$querySuffix};"; $this->resourceConnections[$table] = $connection; } @@ -523,7 +527,7 @@ private function prepareQueryTemplates() * DB connection (if setup). Additionally filters out quote-related queries, if appropriate flag is set. * * @param string $table - * @param array ...$replacements + * @param array $replacements * @return void */ protected function query($table, ... $replacements) @@ -560,6 +564,7 @@ private function getMaxEntityId($tableName, $resourceName, $column = 'entity_id' /** @var \Magento\Framework\Model\ResourceModel\Db\VersionControl\AbstractDb $resource */ $resource = $this->fixtureModel->getObjectManager()->get($resourceName); $connection = $resource->getConnection(); + // phpcs:ignore Magento2.SQL.RawQuery return (int)$connection->query("SELECT MAX(`{$column}`) FROM `{$tableName}`;")->fetchColumn(0); } @@ -570,7 +575,7 @@ private function getMaxEntityId($tableName, $resourceName, $column = 'entity_id' * @param string $typeId * @param int $limit * @return array - * @throws \Exception + * @throws \RuntimeException */ private function getProductIds(\Magento\Store\Api\Data\StoreInterface $store, $typeId, $limit = null) { @@ -590,7 +595,7 @@ private function getProductIds(\Magento\Store\Api\Data\StoreInterface $store, $t } $ids = $productCollection->getAllIds($limit); if ($limit && count($ids) < $limit) { - throw new \Exception('Not enough products of type: ' . $typeId); + throw new \RuntimeException('Not enough products of type: ' . $typeId); } return $ids; } @@ -718,7 +723,7 @@ private function commitBatch() } /** - * {@inheritdoc} + * @inheritdoc */ public function getActionTitle() { @@ -726,7 +731,7 @@ public function getActionTitle() } /** - * {@inheritdoc} + * @inheritdoc */ public function introduceParamLabels() { @@ -737,6 +742,7 @@ public function introduceParamLabels() /** * Get real table name for db table, validated by db adapter. + * * In case prefix or other features mutating default table names are used. * * @param string $tableName diff --git a/setup/src/Magento/Setup/Fixtures/PriceProvider.php b/setup/src/Magento/Setup/Fixtures/PriceProvider.php index c1cb68a4832e2..63ad3f0be6bb2 100644 --- a/setup/src/Magento/Setup/Fixtures/PriceProvider.php +++ b/setup/src/Magento/Setup/Fixtures/PriceProvider.php @@ -19,6 +19,7 @@ class PriceProvider */ public function getPrice($productIndex) { + // phpcs:disable mt_srand($productIndex); switch (mt_rand(0, 3)) { case 0: @@ -30,5 +31,6 @@ public function getPrice($productIndex) case 3: return mt_rand(1, 10000) / 10; } + // phpcs:enable } } diff --git a/setup/src/Magento/Setup/Fixtures/Quote/QuoteGenerator.php b/setup/src/Magento/Setup/Fixtures/Quote/QuoteGenerator.php index 0a63fa5799715..77c8dcb194f75 100644 --- a/setup/src/Magento/Setup/Fixtures/Quote/QuoteGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/Quote/QuoteGenerator.php @@ -85,8 +85,8 @@ class QuoteGenerator /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory - * @param \Magento\ConfigurableProduct\Api\OptionRepositoryInterface $optionRepository * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * @param \Magento\ConfigurableProduct\Api\OptionRepositoryInterface $optionRepository * @param \Magento\ConfigurableProduct\Api\LinkManagementInterface $linkManagement * @param \Magento\Framework\Serialize\SerializerInterface $serializer * @param QuoteConfiguration $config @@ -179,15 +179,15 @@ public function generateQuotes() private function saveQuoteWithQuoteItems($entityId, \Generator $itemIdSequence) { $productCount = [ - Type::TYPE_SIMPLE => mt_rand( + Type::TYPE_SIMPLE => random_int( $this->config->getSimpleCountFrom(), $this->config->getSimpleCountTo() ), - Configurable::TYPE_CODE => mt_rand( + Configurable::TYPE_CODE => random_int( $this->config->getConfigurableCountFrom(), $this->config->getConfigurableCountTo() ), - QuoteConfiguration::BIG_CONFIGURABLE_TYPE => mt_rand( + QuoteConfiguration::BIG_CONFIGURABLE_TYPE => random_int( $this->config->getBigConfigurableCountFrom(), $this->config->getBigConfigurableCountTo() ) @@ -530,6 +530,7 @@ private function prepareProductsForQuote() private function prepareQueryTemplates() { $fileName = $this->config->getFixtureDataFilename(); + // phpcs:ignore Magento2.Functions.DiscouragedFunction $templateData = json_decode(file_get_contents(realpath($fileName)), true); foreach ($templateData as $table => $template) { if (isset($template['_table'])) { @@ -566,6 +567,7 @@ private function prepareQueryTemplates() $connection->beginTransaction(); } + // phpcs:ignore Magento2.SQL.RawQuery $this->queryTemplates[$table] = "INSERT INTO `{$tableName}` ({$fields}) VALUES ({$values}){$querySuffix};"; $this->resourceConnections[$table] = $connection; } @@ -578,7 +580,7 @@ private function prepareQueryTemplates() * DB connection (if setup). Additionally filters out quote-related queries, if appropriate flag is set. * * @param string $table - * @param array ...$replacements + * @param array $replacements * @return void */ protected function query($table, ... $replacements) @@ -606,6 +608,7 @@ private function getMaxEntityId($tableName, $resourceName, $column = 'entity_id' { $tableName = $this->getTableName($tableName, $resourceName); $connection = $this->getConnection($resourceName); + // phpcs:ignore Magento2.SQL.RawQuery return (int)$connection->query("SELECT MAX(`{$column}`) FROM `{$tableName}`;")->fetchColumn(0); } @@ -774,6 +777,7 @@ private function getItemIdSequence($maxItemId, $requestedOrders, $maxItemsPerOrd /** * Get real table name for db table, validated by db adapter. + * * In case prefix or other features mutating default table names are used. * * @param string $tableName diff --git a/setup/src/Magento/Setup/Fixtures/SimpleProductsFixture.php b/setup/src/Magento/Setup/Fixtures/SimpleProductsFixture.php index 62c76d8a2fe11..8e2e842a7d805 100644 --- a/setup/src/Magento/Setup/Fixtures/SimpleProductsFixture.php +++ b/setup/src/Magento/Setup/Fixtures/SimpleProductsFixture.php @@ -132,7 +132,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getActionTitle() { @@ -140,7 +140,7 @@ public function getActionTitle() } /** - * {@inheritdoc} + * @inheritdoc */ public function introduceParamLabels() { @@ -150,7 +150,8 @@ public function introduceParamLabels() } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD) */ public function execute() @@ -185,9 +186,11 @@ public function execute() $additionalAttributeSets = $this->getAdditionalAttributeSets(); $attributeSet = function ($index) use ($defaultAttributeSets, $additionalAttributeSets) { + // phpcs:ignore mt_srand($index); $attributeSetCount = count(array_keys($defaultAttributeSets)); if ($attributeSetCount > (($index - 1) % (int)$this->fixtureModel->getValue('categories', 30))) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction return array_keys($defaultAttributeSets)[mt_rand(0, count(array_keys($defaultAttributeSets)) - 1)]; } else { $customSetsAmount = count($additionalAttributeSets); @@ -205,9 +208,11 @@ public function execute() $additionalAttributeSets ) { $attributeValues = []; + // phpcs:ignore mt_srand($index); if (isset($defaultAttributeSets[$attributeSetId])) { foreach ($defaultAttributeSets[$attributeSetId] as $attributeCode => $values) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $attributeValues[$attributeCode] = $values[mt_rand(0, count($values) - 1)]; } } diff --git a/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php b/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php index 5c43950d49bde..ba57c95999284 100644 --- a/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php +++ b/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php @@ -48,7 +48,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function generateEntity() { @@ -67,7 +67,7 @@ public function generateEntity() */ private function getCustomerTemplate() { - $customerRandomizerNumber = crc32(mt_rand(1, PHP_INT_MAX)); + $customerRandomizerNumber = crc32(random_int(1, PHP_INT_MAX)); $now = new \DateTime(); @@ -100,6 +100,8 @@ private function getCustomerTemplate() } /** + * Get address template. + * * @param int $customerId * @return Address */ diff --git a/setup/src/Magento/Setup/Test/Unit/Model/DataGeneratorTest.php b/setup/src/Magento/Setup/Test/Unit/Model/DataGeneratorTest.php index 4e02fcea2d72a..6888ab2858876 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/DataGeneratorTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/DataGeneratorTest.php @@ -8,6 +8,9 @@ use Magento\Setup\Model\DataGenerator; +/** + * Class DataGeneratorTest + */ class DataGeneratorTest extends \PHPUnit\Framework\TestCase { @@ -38,7 +41,7 @@ public function testGenerateWithKey() $key = 'generate-test'; $data = file(__DIR__ . self::PATH_TO_CSV_FILE); - $wordCount = mt_rand(1, count($data)); + $wordCount = random_int(1, count($data)); $model = new DataGenerator(__DIR__ . self::PATH_TO_CSV_FILE); $result = $model->generate($wordCount, $wordCount, $key);